Skip to content

feat(step-up): record step-up methods in the session auth context - #529

Merged
Bccorb merged 2 commits into
mainfrom
feat/step-up-auth-context
Oct 31, 2026
Merged

Bccorb merged 2 commits into
mainfrom
feat/step-up-auth-context

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

What

The API side of #510 (ADR 0001, D3, D5, D9). Step-up now feeds the session auth context that /oidc/sign and GET /oidc/sessions/:id/auth-context derive acr, amr and auth_time from.

  • recordStepUpVerification adds the step-up's method to sessions.authMethods: a passkey step-up as { method: 'passkey', at, deviceBound, userVerified } (from credentialDeviceType and the UV flag, the same sources sign-in uses), a TOTP step-up (/totp/verify-mfa) as { method: 'totp', at }. A repeat of the same kind replaces its earlier record, so the list stays bounded.
  • authTime moves to the step-up only when the step-up on its own earns at least the session's current acr (none < phr < phrh). auth_time is read as the time of the authentication acr describes, so a TOTP step-up on a passkey session must not make the passkey look recent. A session without an acr gets a fresh auth_time from any step-up.
  • getOidcAuthContext now reports the current row of the refresh chain (falling back to the recorded row when the chain is dead). Step-up writes to the row the access token names, which after a refresh is later than the row the provider recorded, and /oidc/sign already checks the current row. Before this, a session refreshed and then stepped up would report stale values that /oidc/sign then refused. Both now share one auth_time rule (authTime ?? chainStartedAt ?? createdAt).

No route, schema, types or migration change. npm run generate:api leaves openapi.json and adapter-manifest.json unchanged.

acr_values and max_age

Per ADR D3 and D9 these are evaluated by the edge, which holds the authorization request. The API's part is to record step-up and sign only what the session earned. What the edge must do:

  1. Advertise acr_values_supported: ["phr", "phrh"]; phrh satisfies a request for phr.
  2. Read GET /oidc/sessions/:id/auth-context?clientId= (after saving the Grant). max_age=N is met iff now - authTime <= N; acr_values is met iff the reported acr ranks at least the requested value. max_age=0 or prompt=login is never met by an existing session.
  3. If not met, run step-up with the user's first-party access token through the edge adapter: /step-up/webauthn/start and /finish (needed for any acr, and for freshness on a session that already has an acr), or /totp/verify-mfa (freshness only, on a session without an acr). Both are access passthroughs in the adapter manifest.
  4. Re-read auth-context after step-up and copy acr, amr and auth_time verbatim. /oidc/sign refuses anything else with auth_context_mismatch.
  5. If the requirement is still unmet (for example a synced passkey when phrh is essential), answer unmet_authentication_requirements rather than issuing a lower acr.

Not in this PR

Refs #510

Checks

  • npm run typecheck: clean
  • npm run lint: clean
  • npm run format:check: clean
  • npm run coverage: 164 files, 2543 passed, 1 todo; 98.99% statements, 95.7% branches
  • TEST_DB=postgres suite on a throwaway Postgres 18 after the full migration chain: 8 files, 48 passed, including a step-up on a refreshed row reported through the originally recorded session id
  • Security review of the diff: one finding (TOTP step-up refreshing auth_time under an older passkey acr), fixed in the second commit with a test

A passkey or TOTP step-up now adds its method to sessions.authMethods
and moves authTime, so the provider acr, amr and auth_time reflect it.
The auth-context lookup reads the current row of the refresh chain,
where step-up records and what /oidc/sign checks.

Refs #510
A TOTP step-up on a passkey session no longer refreshes auth_time, so
the signed acr is never paired with the time of a weaker factor.

Refs #510
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant