const debug = require('ghost-ignition').debug('web:oauth:app'); const {URL} = require('url'); const passport = require('passport'); const GoogleStrategy = require('passport-google-oauth20').Strategy; const express = require('../../../shared/express'); const urlUtils = require('../../../shared/url-utils'); const shared = require('../shared'); const config = require('../../../shared/config'); const settingsCache = require('../../services/settings/cache'); const models = require('../../models'); const auth = require('../../services/auth'); function randomPassword() { return require('crypto').randomBytes(128).toString('hex'); } module.exports = function setupOAuthApp() { debug('OAuth App setup start'); const oauthApp = express('oauth'); if (!config.get('enableDeveloperExperiments')) { debug('OAuth App setup skipped'); return oauthApp; } // send 503 json response in case of maintenance oauthApp.use(shared.middlewares.maintenance); /** * Configure the passport.authenticate middleware * We need to configure it on each request because clientId and secret * will change (when the Owner is changing these settings) */ function googleOAuthMiddleware(clientId, secret) { return (req, res, next) => { // TODO: use url config instead of the string /ghost //Create the callback url to be sent to Google const callbackUrl = new URL(urlUtils.getSiteUrl()); callbackUrl.pathname = '/ghost/oauth/google/callback'; passport.authenticate(new GoogleStrategy({ clientID: clientId, clientSecret: secret, callbackURL: callbackUrl.href }, async function (accessToken, refreshToken, profile) { // This is the verify function that checks that a Google-authenticated user // is matching one of our users (or invite). if (req.user) { // CASE: the user already has an active Ghost session const emails = profile.emails.filter(email => email.verified === true).map(email => email.value); if (!emails.includes(req.user.get('email'))) { return res.redirect('/ghost/#/staff/?message=oauth-linking-failed'); } // TODO: configure the oauth data for this user (row in the oauth table) //Associate logged-in user with oauth account req.user.set('password', randomPassword()); await req.user.save(); } else { // CASE: the user is logging-in or accepting an invite //Find user in DB and log-in //TODO: instead find the oauth row with the email use the provider id const emails = profile.emails.filter(email => email.verified === true); if (emails.length < 1) { return res.redirect('/ghost/#/signin?message=login-failed'); } const email = emails[0].value; let user = await models.User.findOne({ email: email }); if (!user) { // CASE: the user is accepting an invite // TODO: move this code in the invitations service const options = {context: {internal: true}}; let invite = await models.Invite.findOne({email, status: 'sent'}, options); if (!invite || invite.get('expires') < Date.now()) { return res.redirect('/ghost/#/signin?message=login-failed'); } //Accept invite user = await models.User.add({ email: email, name: profile.displayName, password: randomPassword(), roles: [invite.toJSON().role_id] }, options); await invite.destroy(options); // TODO: create an oauth model link to user } req.user = user; } await auth.session.sessionService.createSessionForUser(req, res, req.user); return res.redirect('/ghost/'); }), { scope: ['profile', 'email'], session: false, prompt: 'consent', accessType: 'offline' })(req, res, next); }; } oauthApp.get('/:provider', auth.authenticate.authenticateAdminApi, (req, res, next) => { if (req.params.provider !== 'google') { return res.sendStatus(404); } const clientId = settingsCache.get('oauth_client_id'); const secret = settingsCache.get('oauth_client_secret'); if (clientId && secret) { return googleOAuthMiddleware(clientId, secret)(req, res, next); } res.sendStatus(404); }); oauthApp.get('/:provider/callback', (req, res, next) => { // Bypass CSRF protection to authenticate users as they are redirected from // Google OAuth consent screen res.locals.bypassCsrfProtection = true; next(); }, auth.authenticate.authenticateAdminApi, (req, res, next) => { if (req.params.provider !== 'google') { return res.sendStatus(404); } const clientId = settingsCache.get('oauth_client_id'); const secret = settingsCache.get('oauth_client_secret'); if (clientId && secret) { return googleOAuthMiddleware(clientId, secret)(req, res, next); } res.sendStatus(404); }); debug('OAuth App setup end'); return oauthApp; };