mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Changed SSO adapter to automatically verify sessions (#21388)
ref ENG-1680 SSO is a different flow that wouldn't need the extra email verification flow
This commit is contained in:
parent
ea11367ea4
commit
07afa6500d
6 changed files with 164 additions and 9 deletions
|
@ -59,15 +59,17 @@ const sessionService = createSessionService({
|
|||
|
||||
module.exports = createSessionMiddleware({sessionService});
|
||||
|
||||
const ssoAdapter = adapterManager.getAdapter('sso');
|
||||
// Looks funky but this is a "custom" piece of middleware
|
||||
module.exports.createSessionFromToken = sessionFromToken({
|
||||
callNextWithError: false,
|
||||
createSession: sessionService.createSessionForUser,
|
||||
findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter),
|
||||
getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter),
|
||||
getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter)
|
||||
});
|
||||
module.exports.createSessionFromToken = () => {
|
||||
const ssoAdapter = adapterManager.getAdapter('sso');
|
||||
return sessionFromToken({
|
||||
callNextWithError: false,
|
||||
createSession: sessionService.createVerifiedSessionForUser,
|
||||
findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter),
|
||||
getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter),
|
||||
getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter)
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.sessionService = sessionService;
|
||||
module.exports.deleteAllSessions = expressSession.deleteAllSessions;
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = () => {
|
|||
backendApp.lazyUse(BASE_API_PATH, require('../api'));
|
||||
backendApp.lazyUse('/ghost/.well-known', require('../well-known'));
|
||||
|
||||
backendApp.use('/ghost', require('../../services/auth/session').createSessionFromToken, require('../admin')());
|
||||
backendApp.use('/ghost', require('../../services/auth/session').createSessionFromToken(), require('../admin')());
|
||||
|
||||
return backendApp;
|
||||
};
|
||||
|
|
16
ghost/core/test/e2e-api/admin/__snapshots__/sso.test.js.snap
Normal file
16
ghost/core/test/e2e-api/admin/__snapshots__/sso.test.js.snap
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SSO API SSO with 2FA enabled can sign in with SSO when 2FA is enabled 1: [headers] 1`] = `
|
||||
Object {
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": 156,
|
||||
"content-security-policy": "default-src 'none'",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"set-cookie": Array [
|
||||
StringMatching /\\^ghost-admin-api-session=/,
|
||||
],
|
||||
"vary": "Accept-Encoding",
|
||||
"x-content-type-options": "nosniff",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
77
ghost/core/test/e2e-api/admin/sso.test.js
Normal file
77
ghost/core/test/e2e-api/admin/sso.test.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {mockLabsEnabled, mockLabsDisabled, restore} = require('../../utils/e2e-framework-mock-manager');
|
||||
const {stringMatching} = matchers;
|
||||
const sinon = require('sinon');
|
||||
const adapterManager = require('../../../core/server/services/adapter-manager');
|
||||
const models = require('../../../core/server/models');
|
||||
|
||||
describe('SSO API', function () {
|
||||
let agent;
|
||||
|
||||
before(async function () {
|
||||
// Configure mock SSO adapter that always returns owner
|
||||
const owner = await models.User.getOwnerUser();
|
||||
|
||||
// Create a mock adapter that always returns the owner
|
||||
class MockSSOAdapter {
|
||||
async getRequestCredentials() {
|
||||
return {
|
||||
id: 'mock-credentials'
|
||||
};
|
||||
}
|
||||
|
||||
async getIdentityFromCredentials() {
|
||||
return {
|
||||
id: 'mock-identity'
|
||||
};
|
||||
}
|
||||
|
||||
async getUserForIdentity() {
|
||||
return owner;
|
||||
}
|
||||
}
|
||||
|
||||
// Stub adapter manager to return mock SSO adapter
|
||||
const originalGetAdapter = adapterManager.getAdapter;
|
||||
sinon.stub(adapterManager, 'getAdapter').callsFake((name) => {
|
||||
if (name === 'sso') {
|
||||
return new MockSSOAdapter();
|
||||
}
|
||||
return originalGetAdapter.call(this, name);
|
||||
});
|
||||
|
||||
agent = await agentProvider.getGhostAPIAgent();
|
||||
await fixtureManager.init();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
restore();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('SSO with 2FA enabled', function () {
|
||||
beforeEach(async function () {
|
||||
mockLabsEnabled('staff2fa');
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
mockLabsDisabled('staff2fa');
|
||||
});
|
||||
|
||||
it('can sign in with SSO when 2FA is enabled', async function () {
|
||||
await agent
|
||||
.post('/')
|
||||
.expectEmptyBody()
|
||||
.matchHeaderSnapshot({
|
||||
'set-cookie': [
|
||||
stringMatching(/^ghost-admin-api-session=/)
|
||||
]
|
||||
});
|
||||
|
||||
// Verify we can access authenticated endpoints after SSO login
|
||||
await agent
|
||||
.get('api/admin/users/me')
|
||||
.expectStatus(200);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -41,6 +41,7 @@ totp.options = {
|
|||
* @prop {(req: Req, res: Res) => Promise<User | null>} getUserForSession
|
||||
* @prop {(req: Req, res: Res) => Promise<void>} removeUserForSession
|
||||
* @prop {(req: Req, res: Res, user: User) => Promise<void>} createSessionForUser
|
||||
* @prop {(req: Req, res: Res) => Promise<void>} createVerifiedSessionForUser
|
||||
* @prop {(req: Req, res: Res) => Promise<void>} verifySession
|
||||
* @prop {(req: Req, res: Res) => Promise<void>} sendAuthCodeToUser
|
||||
* @prop {(req: Req, res: Res) => Promise<boolean>} verifyAuthCodeForUser
|
||||
|
@ -122,6 +123,19 @@ module.exports = function createSessionService({
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createVerifiedSessionForUser
|
||||
*
|
||||
* @param {Req} req
|
||||
* @param {Res} res
|
||||
* @param {User} user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function createVerifiedSessionForUser(req, res, user) {
|
||||
await createSessionForUser(req, res, user);
|
||||
await verifySession(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* generateAuthCodeForUser
|
||||
*
|
||||
|
@ -328,6 +342,7 @@ module.exports = function createSessionService({
|
|||
return {
|
||||
getUserForSession,
|
||||
createSessionForUser,
|
||||
createVerifiedSessionForUser,
|
||||
removeUserForSession,
|
||||
verifySession,
|
||||
isVerifiedSession,
|
||||
|
|
|
@ -472,4 +472,49 @@ describe('SessionService', function () {
|
|||
message: 'Failed to send email. Please check your site configuration and try again.'
|
||||
});
|
||||
});
|
||||
|
||||
it('Can create a verified session for SSO', async function () {
|
||||
const getSession = async (req) => {
|
||||
if (req.session) {
|
||||
return req.session;
|
||||
}
|
||||
req.session = {
|
||||
destroy: sinon.spy(cb => cb())
|
||||
};
|
||||
return req.session;
|
||||
};
|
||||
const findUserById = sinon.spy(async ({id}) => ({id}));
|
||||
const getOriginOfRequest = sinon.stub().returns('origin');
|
||||
const labs = {
|
||||
isSet: () => false
|
||||
};
|
||||
|
||||
const sessionService = SessionService({
|
||||
getSession,
|
||||
findUserById,
|
||||
getOriginOfRequest,
|
||||
labs
|
||||
});
|
||||
|
||||
const req = Object.create(express.request, {
|
||||
ip: {
|
||||
value: '0.0.0.0'
|
||||
},
|
||||
headers: {
|
||||
value: {
|
||||
cookie: 'thing'
|
||||
}
|
||||
},
|
||||
get: {
|
||||
value: () => 'Fake'
|
||||
}
|
||||
});
|
||||
const res = Object.create(express.response);
|
||||
const user = {id: 'egg'};
|
||||
|
||||
await sessionService.createVerifiedSessionForUser(req, res, user);
|
||||
|
||||
should.equal(req.session.user_id, 'egg');
|
||||
should.equal(req.session.verified, true);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue