Make SSO session exchange fail closed #48

Open
opened 2026-07-24 22:48:07 +00:00 by frank · 0 comments
Owner

Goal

Allow the SSO session middleware to reject invalid external identities instead of falling through to an existing Ghost session.

Requirements

  • Update ghost/core/core/server/services/auth/session/session-from-token.js.

  • Add a strict or fail-closed mode.

  • In strict mode, reject requests when:

    • credentials are missing;
    • credentials are invalid or unauthorized;
    • no matching Ghost user exists.
  • Remove the Ghost user from the current session before rejecting the request.

  • Preserve the existing fallthrough behavior when strict mode is disabled.

Acceptance criteria

  • Missing credentials cannot leave an existing Ghost session usable in strict mode.
  • Invalid or unauthorized identities are rejected.
  • Unknown Ghost users are rejected.
  • The existing Ghost session is cleared before rejection.
  • Non-strict adapters retain the existing behavior.
  • Unit tests cover each rejection path.

Further information and implementation sketch

The current middleware returns next() when one of the SSO stages returns no result:

const token = await getTokenFromRequest(req);

if (!token) {
    return next();
}

const identity = await getLookupFromToken(token);

if (!identity) {
    return next();
}

const user = await findUserByLookup(identity);

if (!user) {
    return next();
}

await createSession(req, res, user);
next();

This leaves any existing Ghost session untouched.

A possible extension is to inject a session-removal function and enable strict handling explicitly:

function SessionFromToken({
    getTokenFromRequest,
    getLookupFromToken,
    findUserByLookup,
    createSession,
    removeUserForSession,
    strict = false,
    callNextWithError
}) {
    async function reject(req, res, next) {
        await removeUserForSession(req, res);

        const error = new errors.NoPermissionError({
            message: 'External identity is not authorized.'
        });

        return callNextWithError ? next(error) : next();
    }

    async function handler(req, res, next) {
        try {
            const token = await getTokenFromRequest(req);

            if (!token) {
                return strict ? reject(req, res, next) : next();
            }

            const identity = await getLookupFromToken(token);

            if (!identity) {
                return strict ? reject(req, res, next) : next();
            }

            const user = await findUserByLookup(identity);

            if (!user) {
                return strict ? reject(req, res, next) : next();
            }

            await createSession(req, res, user);
            next();
        } catch (err) {
            if (callNextWithError) {
                return next(err);
            }

            next();
        }
    }

    return handler;
}

The exact error and fallback behavior should be decided together with the Admin application behavior. The security requirement is that strict-mode failure clears the existing Ghost identity and does not silently continue as that user.

Relevant implementation:

  • ghost/core/core/server/services/auth/session/session-from-token.js
  • ghost/core/test/unit/server/services/auth/session-from-token.test.js
## Goal Allow the SSO session middleware to reject invalid external identities instead of falling through to an existing Ghost session. ## Requirements * Update `ghost/core/core/server/services/auth/session/session-from-token.js`. * Add a strict or fail-closed mode. * In strict mode, reject requests when: * credentials are missing; * credentials are invalid or unauthorized; * no matching Ghost user exists. * Remove the Ghost user from the current session before rejecting the request. * Preserve the existing fallthrough behavior when strict mode is disabled. ## Acceptance criteria * [ ] Missing credentials cannot leave an existing Ghost session usable in strict mode. * [ ] Invalid or unauthorized identities are rejected. * [ ] Unknown Ghost users are rejected. * [ ] The existing Ghost session is cleared before rejection. * [ ] Non-strict adapters retain the existing behavior. * [ ] Unit tests cover each rejection path. ## Further information and implementation sketch The current middleware returns `next()` when one of the SSO stages returns no result: ```js const token = await getTokenFromRequest(req); if (!token) { return next(); } const identity = await getLookupFromToken(token); if (!identity) { return next(); } const user = await findUserByLookup(identity); if (!user) { return next(); } await createSession(req, res, user); next(); ``` This leaves any existing Ghost session untouched. A possible extension is to inject a session-removal function and enable strict handling explicitly: ```js function SessionFromToken({ getTokenFromRequest, getLookupFromToken, findUserByLookup, createSession, removeUserForSession, strict = false, callNextWithError }) { async function reject(req, res, next) { await removeUserForSession(req, res); const error = new errors.NoPermissionError({ message: 'External identity is not authorized.' }); return callNextWithError ? next(error) : next(); } async function handler(req, res, next) { try { const token = await getTokenFromRequest(req); if (!token) { return strict ? reject(req, res, next) : next(); } const identity = await getLookupFromToken(token); if (!identity) { return strict ? reject(req, res, next) : next(); } const user = await findUserByLookup(identity); if (!user) { return strict ? reject(req, res, next) : next(); } await createSession(req, res, user); next(); } catch (err) { if (callNextWithError) { return next(err); } next(); } } return handler; } ``` The exact error and fallback behavior should be decided together with the Admin application behavior. The security requirement is that strict-mode failure clears the existing Ghost identity and does not silently continue as that user. Relevant implementation: * `ghost/core/core/server/services/auth/session/session-from-token.js` * `ghost/core/test/unit/server/services/auth/session-from-token.test.js`
frank added this to the Forward Auth mode milestone 2026-07-24 22:48:07 +00:00
Sign in to join this conversation.
No description provided.