Ghost/core/server/web/oauth/app.js
Thibaut Patel 2bcc934eb4 Disable CSRF on the oauth callback route
no issue

Keeping CSRF enabled there would prevent oauth from working as users are redirected from the provider domain to the /callback route, where they are logged-in
2021-05-18 20:44:21 +02:00

154 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) => {
// 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;
};