2021-04-21 20:33:48 +03:00
|
|
|
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);
|
|
|
|
|
2021-05-14 13:10:27 +03:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
*/
|
2021-04-21 20:33:48 +03:00
|
|
|
function googleOAuthMiddleware(clientId, secret) {
|
|
|
|
return (req, res, next) => {
|
2021-05-14 13:10:27 +03:00
|
|
|
// TODO: use url config instead of the string /ghost
|
|
|
|
|
|
|
|
//Create the callback url to be sent to Google
|
2021-04-21 20:33:48 +03:00
|
|
|
const callbackUrl = new URL(urlUtils.getSiteUrl());
|
|
|
|
callbackUrl.pathname = '/ghost/oauth/google/callback';
|
2021-05-14 13:10:27 +03:00
|
|
|
|
2021-04-21 20:33:48 +03:00
|
|
|
passport.authenticate(new GoogleStrategy({
|
|
|
|
clientID: clientId,
|
|
|
|
clientSecret: secret,
|
|
|
|
callbackURL: callbackUrl.href
|
2021-04-21 20:45:03 +03:00
|
|
|
}, async function (accessToken, refreshToken, profile) {
|
2021-05-14 13:10:27 +03:00
|
|
|
// This is the verify function that checks that a Google-authenticated user
|
|
|
|
// is matching one of our users (or invite).
|
|
|
|
|
2021-04-21 20:33:48 +03:00
|
|
|
if (req.user) {
|
2021-05-14 13:10:27 +03:00
|
|
|
// CASE: the user already has an active Ghost session
|
2021-04-21 20:33:48 +03:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2021-05-14 13:10:27 +03:00
|
|
|
// TODO: configure the oauth data for this user (row in the oauth table)
|
|
|
|
|
2021-04-21 20:33:48 +03:00
|
|
|
//Associate logged-in user with oauth account
|
|
|
|
req.user.set('password', randomPassword());
|
|
|
|
await req.user.save();
|
|
|
|
} else {
|
2021-05-14 13:10:27 +03:00
|
|
|
// CASE: the user is logging-in or accepting an invite
|
|
|
|
|
2021-04-21 20:33:48 +03:00
|
|
|
//Find user in DB and log-in
|
2021-05-14 13:10:27 +03:00
|
|
|
//TODO: instead find the oauth row with the email use the provider id
|
2021-04-21 20:33:48 +03:00
|
|
|
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) {
|
2021-05-14 13:10:27 +03:00
|
|
|
// CASE: the user is accepting an invite
|
|
|
|
// TODO: move this code in the invitations service
|
2021-04-21 20:33:48 +03:00
|
|
|
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);
|
2021-05-14 13:10:27 +03:00
|
|
|
|
|
|
|
// TODO: create an oauth model link to user
|
2021-04-21 20:33:48 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
req.user = user;
|
|
|
|
}
|
|
|
|
|
|
|
|
await auth.session.sessionService.createSessionForUser(req, res, req.user);
|
|
|
|
|
|
|
|
return res.redirect('/ghost/');
|
|
|
|
}), {
|
|
|
|
scope: ['profile', 'email'],
|
2021-04-23 12:20:40 +03:00
|
|
|
session: false,
|
|
|
|
prompt: 'consent',
|
|
|
|
accessType: 'offline'
|
2021-04-21 20:33:48 +03:00
|
|
|
})(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);
|
|
|
|
});
|
|
|
|
|
2021-05-18 21:44:13 +03:00
|
|
|
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) => {
|
2021-04-21 20:33:48 +03:00
|
|
|
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;
|
|
|
|
};
|