diff --git a/forge/ee/routes/sso/social/google.js b/forge/ee/routes/sso/social/google.js index a2f1463e17..8d0571d6d8 100644 --- a/forge/ee/routes/sso/social/google.js +++ b/forge/ee/routes/sso/social/google.js @@ -49,20 +49,24 @@ module.exports = fp(async function (app, opts) { }) const googleUserInfo = userinfo.data await googleJWTCache.set(request.query.code, {}) + if (googleUserInfo.email_verified !== true) { + reply.code(403).send({ code: 'unverified_email', error: 'Google account email is not verified' }) + return + } + // Any email on a domain with an SSO provider must go through that provider, + // whether or not Google reports a Workspace (hd) account + const providerId = await app.sso.getProviderForEmail(googleUserInfo.email) + if (providerId) { + reply.send({ + url: `/ee/sso/login?u=${encodeURIComponent(googleUserInfo.email)}` + }) + return + } if (googleUserInfo.hd) { - // This is a Google Workspace account - check if there is an SSO provider configured for this domain - const providerId = await app.sso.getProviderForEmail(googleUserInfo.email) - if (!providerId) { - // No SSO provider configured for this domain - redirect to home - reply.send({ - error: 'SSO not available for this Google Workspace domain' - }) - } else { - // There is an SSO provider configured for this domain - use it - reply.send({ - url: `/ee/sso/login?u=${encodeURIComponent(googleUserInfo.email)}` - }) - } + // Workspace account on a domain with no SSO provider configured + reply.send({ + error: 'SSO not available for this Google Workspace domain' + }) return } const user = await app.db.models.User.byUsernameOrEmail(googleUserInfo.email) diff --git a/test/unit/forge/ee/routes/sso/google_spec.js b/test/unit/forge/ee/routes/sso/google_spec.js new file mode 100644 index 0000000000..b7cd2d59d2 --- /dev/null +++ b/test/unit/forge/ee/routes/sso/google_spec.js @@ -0,0 +1,88 @@ +const { OAuth2Client } = require('google-auth-library') +const should = require('should') // eslint-disable-line +const sinon = require('sinon') + +const setup = require('../../setup') + +describe('Google SSO callback', function () { + let app + let userinfo + let codeCounter = 0 + + before(async function () { + app = await setup({ billing: undefined }) + await app.settings.set('platform:sso:google', true) + await app.settings.set('platform:sso:google:clientId', 'test-client-id') + + await app.db.models.User.create({ username: 'carol', name: 'Carol', email: 'carol@corp.com', email_verified: true, password: 'ccPassword1!' }) + await app.db.models.User.create({ username: 'dave', name: 'Dave', email: 'dave@plain.com', email_verified: true, password: 'ddPassword1!' }) + await app.db.models.SAMLProvider.create({ + name: 'corp.com', + domainFilter: '@corp.com', + active: true, + options: { + cert: '-----BEGIN CERTIFICATE-----abcde-----END CERTIFICATE-----', + entryPoint: 'https://sso.corp.com/entry' + } + }) + + sinon.stub(OAuth2Client.prototype, 'getTokenInfo').resolves({ aud: 'test-client-id' }) + sinon.stub(OAuth2Client.prototype, 'request').callsFake(async () => ({ data: userinfo })) + }) + + after(async function () { + sinon.restore() + await app.close() + }) + + async function googleCallback (info) { + userinfo = info + return app.inject({ + method: 'POST', + url: `/ee/sso/login/callback/google?code=code-${codeCounter++}` + }) + } + + it('redirects a consumer account on an SSO enforced domain to the provider', async function () { + const response = await googleCallback({ email: 'carol@corp.com', email_verified: true }) + response.statusCode.should.equal(200) + response.json().should.have.property('url', `/ee/sso/login?u=${encodeURIComponent('carol@corp.com')}`) + response.cookies.should.have.length(0) + }) + + it('redirects a Workspace account on an SSO enforced domain to the provider', async function () { + const response = await googleCallback({ email: 'carol@corp.com', email_verified: true, hd: 'corp.com' }) + response.statusCode.should.equal(200) + response.json().should.have.property('url', `/ee/sso/login?u=${encodeURIComponent('carol@corp.com')}`) + response.cookies.should.have.length(0) + }) + + it('rejects an account whose email Google reports as unverified', async function () { + const response = await googleCallback({ email: 'dave@plain.com', email_verified: false }) + response.statusCode.should.equal(403) + response.json().should.have.property('code', 'unverified_email') + response.cookies.should.have.length(0) + }) + + it('rejects an account with no email_verified claim', async function () { + const response = await googleCallback({ email: 'dave@plain.com' }) + response.statusCode.should.equal(403) + response.json().should.have.property('code', 'unverified_email') + response.cookies.should.have.length(0) + }) + + it('returns an error for a Workspace account on a domain with no SSO provider', async function () { + const response = await googleCallback({ email: 'dave@plain.com', email_verified: true, hd: 'plain.com' }) + response.statusCode.should.equal(200) + response.json().should.have.property('error', 'SSO not available for this Google Workspace domain') + response.cookies.should.have.length(0) + }) + + it('signs in an existing user on a domain with no SSO provider', async function () { + const response = await googleCallback({ email: 'dave@plain.com', email_verified: true }) + response.statusCode.should.equal(200) + response.json().should.have.property('url', '/') + response.cookies.should.have.length(1) + response.cookies[0].name.should.equal('sid') + }) +})