Ghost/core/server/web/oauth/app.js
Thibaut Patel 1d5c8ccf2b Fix the OAuth callback
https://github.com/TryGhost/Team/issues/614

By binding the user session to the Ghost instance domain
2021-05-21 10:50:03 +02:00

153 lines
5.8 KiB
JavaScript

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) => {
// Set the referrer as the ghost instance domain so that the session is linked to the ghost instance domain
req.headers.referrer = urlUtils.getSiteUrl();
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;
};