Add configurable strict SSO session handling #49

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

Goal

Wire strict session handling into Ghost’s SSO adapter integration without changing the behavior of existing adapters.

Requirements

  • Update ghost/core/core/server/services/auth/session/index.js.
  • Pass removeUserForSession into the SSO exchange middleware.
  • Allow the active SSO adapter or its configuration to enable strict mode.
  • Keep strict mode disabled by default for compatibility.
  • Add a typed capability to SSOBase if strict mode should become part of the supported adapter contract.
  • Validate the strict-mode configuration during startup.

Acceptance criteria

  • An SSO adapter can explicitly enable strict mode.
  • The SSO middleware receives access to session invalidation.
  • Existing adapters continue to load without changes.
  • Strict mode is disabled by default.
  • Invalid strict-mode configuration fails during startup.
  • Tests cover strict and non-strict adapters.

Further information and implementation sketch

Ghost currently constructs the SSO middleware in:

ghost/core/core/server/services/auth/session/index.js

The relevant wiring currently resembles:

return sessionFromToken({
    callNextWithError: false,
    createSession: sessionService.createVerifiedSessionForUser,
    findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter),
    getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter),
    getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter)
});

A minimal internal implementation could read an optional adapter property:

const strict = ssoAdapter.failClosed === true;

return sessionFromToken({
    strict,
    callNextWithError: strict,
    removeUserForSession: sessionService.removeUserForSession,
    createSession: sessionService.createVerifiedSessionForUser,
    findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter),
    getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter),
    getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter)
});

An adapter could then opt in with:

export class NiceOAuthForwardAuth extends SSOBase<
  ForwardAuthCredentials,
  string
> {
  readonly failClosed = true

  // ...
}

For a cleaner public contract, the base class could expose an optional capability:

export abstract class SSOBase<Token, Lookup>
implements SSOAdapter<Token, Lookup> {
  readonly failClosed: boolean = false

  // ...
}

Alternatively, strict mode could be supplied through adapter configuration:

{
  "adapters": {
    "sso": {
      "active": "nice-oauth-forward-auth",
      "nice-oauth-forward-auth": {
        "failClosed": true
      }
    }
  }
}

The constructor would retain the setting:

type ForwardAuthConfig = {
  failClosed?: boolean
  emailHeader?: string
  groupsHeader?: string
  requiredGroups?: string[]
}

export class NiceOAuthForwardAuth extends SSOBase<
  ForwardAuthCredentials,
  string
> {
  readonly failClosed: boolean

  constructor(config: ForwardAuthConfig = {}) {
    super()
    this.failClosed = config.failClosed ?? true
  }
}

Whether this belongs on SSOBase or only in the adapter configuration should be decided before implementation. Putting it on SSOBase makes the behavior discoverable and reusable for other SSO adapters.

Relevant implementation:

  • ghost/core/core/server/services/auth/session/index.js
  • packages/adapters/sso-base/src/base.ts
  • packages/adapters/sso-base/test/index.test.ts
## Goal Wire strict session handling into Ghost’s SSO adapter integration without changing the behavior of existing adapters. ## Requirements * Update `ghost/core/core/server/services/auth/session/index.js`. * Pass `removeUserForSession` into the SSO exchange middleware. * Allow the active SSO adapter or its configuration to enable strict mode. * Keep strict mode disabled by default for compatibility. * Add a typed capability to `SSOBase` if strict mode should become part of the supported adapter contract. * Validate the strict-mode configuration during startup. ## Acceptance criteria * [ ] An SSO adapter can explicitly enable strict mode. * [ ] The SSO middleware receives access to session invalidation. * [ ] Existing adapters continue to load without changes. * [ ] Strict mode is disabled by default. * [ ] Invalid strict-mode configuration fails during startup. * [ ] Tests cover strict and non-strict adapters. ## Further information and implementation sketch Ghost currently constructs the SSO middleware in: ```text ghost/core/core/server/services/auth/session/index.js ``` The relevant wiring currently resembles: ```js return sessionFromToken({ callNextWithError: false, createSession: sessionService.createVerifiedSessionForUser, findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter), getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter), getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter) }); ``` A minimal internal implementation could read an optional adapter property: ```js const strict = ssoAdapter.failClosed === true; return sessionFromToken({ strict, callNextWithError: strict, removeUserForSession: sessionService.removeUserForSession, createSession: sessionService.createVerifiedSessionForUser, findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter), getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter), getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter) }); ``` An adapter could then opt in with: ```ts export class NiceOAuthForwardAuth extends SSOBase< ForwardAuthCredentials, string > { readonly failClosed = true // ... } ``` For a cleaner public contract, the base class could expose an optional capability: ```ts export abstract class SSOBase<Token, Lookup> implements SSOAdapter<Token, Lookup> { readonly failClosed: boolean = false // ... } ``` Alternatively, strict mode could be supplied through adapter configuration: ```json { "adapters": { "sso": { "active": "nice-oauth-forward-auth", "nice-oauth-forward-auth": { "failClosed": true } } } } ``` The constructor would retain the setting: ```ts type ForwardAuthConfig = { failClosed?: boolean emailHeader?: string groupsHeader?: string requiredGroups?: string[] } export class NiceOAuthForwardAuth extends SSOBase< ForwardAuthCredentials, string > { readonly failClosed: boolean constructor(config: ForwardAuthConfig = {}) { super() this.failClosed = config.failClosed ?? true } } ``` Whether this belongs on `SSOBase` or only in the adapter configuration should be decided before implementation. Putting it on `SSOBase` makes the behavior discoverable and reusable for other SSO adapters. Relevant implementation: * `ghost/core/core/server/services/auth/session/index.js` * `packages/adapters/sso-base/src/base.ts` * `packages/adapters/sso-base/test/index.test.ts`
frank added this to the Forward Auth mode milestone 2026-07-24 22:49:02 +00:00
Sign in to join this conversation.
No description provided.