0
Fork 0
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:
Sam Lord 2024-11-11 22:26:40 +00:00 committed by GitHub
parent ea11367ea4
commit 07afa6500d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 164 additions and 9 deletions

View file

@ -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;

View file

@ -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;
};

View 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",
}
`;

View 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);
});
});
});

View file

@ -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,

View file

@ -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);
});
});