diff --git a/core/server/services/auth/session/index.js b/core/server/services/auth/session/index.js index 49c0ddcb99..72462feafc 100644 --- a/core/server/services/auth/session/index.js +++ b/core/server/services/auth/session/index.js @@ -47,3 +47,5 @@ module.exports.createSessionFromToken = sessionFromToken({ getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter), getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter) }); + +module.exports.sessionService = sessionService; diff --git a/core/server/web/oauth/app.js b/core/server/web/oauth/app.js new file mode 100644 index 0000000000..e0f16a0a50 --- /dev/null +++ b/core/server/web/oauth/app.js @@ -0,0 +1,124 @@ +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); + + function googleOAuthMiddleware(clientId, secret) { + return (req, res, next) => { + 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, cb) { + if (req.user) { + 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'); + } + + //Associate logged-in user with oauth account + req.user.set('password', randomPassword()); + await req.user.save(); + } else { + //Find user in DB and log-in + 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) { + 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); + } + + req.user = user; + } + + await auth.session.sessionService.createSessionForUser(req, res, req.user); + + return res.redirect('/ghost/'); + }), { + scope: ['profile', 'email'], + session: false + })(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', 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; +}; diff --git a/core/server/web/oauth/index.js b/core/server/web/oauth/index.js new file mode 100644 index 0000000000..a9612f49fb --- /dev/null +++ b/core/server/web/oauth/index.js @@ -0,0 +1 @@ +module.exports = require('./app'); diff --git a/core/server/web/parent/app.js b/core/server/web/parent/app.js index 199e8114d4..e794c91927 100644 --- a/core/server/web/parent/app.js +++ b/core/server/web/parent/app.js @@ -46,6 +46,7 @@ module.exports = function setupParentApp(options = {}) { // Wrap the admin and API apps into a single express app for use with vhost const backendApp = express('backend'); backendApp.use('/ghost/api', require('../api')()); + backendApp.use('/ghost/oauth', require('../oauth')()); backendApp.use('/ghost/.well-known', require('../well-known')()); backendApp.use('/ghost', require('../../services/auth/session').createSessionFromToken, require('../admin')()); diff --git a/package.json b/package.json index 6b65adf449..50676fc0d4 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,8 @@ "node-jose": "2.0.0", "nodemailer": "0.7.1", "oembed-parser": "1.3.7", + "passport": "^0.4.1", + "passport-google-oauth": "^2.0.0", "path-match": "1.2.4", "probe-image-size": "5.0.0", "rss": "1.2.2", diff --git a/yarn.lock b/yarn.lock index 18ef0c35f2..912809d02e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,7 +1323,7 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64url@^3.0.1: +base64url@3.x.x, base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -6962,6 +6962,11 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= + object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -7276,6 +7281,61 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +passport-google-oauth1@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc" + integrity sha1-r3SoA99R7GRvZqRNgigr5vEI4Mw= + dependencies: + passport-oauth1 "1.x.x" + +passport-google-oauth20@2.x.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" + integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ== + dependencies: + passport-oauth2 "1.x.x" + +passport-google-oauth@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth/-/passport-google-oauth-2.0.0.tgz#f6eb4bc96dd6c16ec0ecfdf4e05ec48ca54d4dae" + integrity sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA== + dependencies: + passport-google-oauth1 "1.x.x" + passport-google-oauth20 "2.x.x" + +passport-oauth1@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918" + integrity sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg= + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + utils-merge "1.x.x" + +passport-oauth2@1.x.x: + version "1.5.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" + integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== + dependencies: + base64url "3.x.x" + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + +passport@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" + integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -7350,6 +7410,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -9433,6 +9498,11 @@ uid-safe@~2.1.5: dependencies: random-bytes "~1.0.0" +uid2@0.0.x: + version "0.0.3" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" + integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -9625,7 +9695,7 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -utils-merge@1.0.1: +utils-merge@1.0.1, utils-merge@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=