Skip to content

feat: add the /oidc/* service routes and tables for the OpenID Provider service - #521

Merged
Bccorb merged 6 commits into
mainfrom
feat/oidc-edge-routes
Oct 30, 2026
Merged

Bccorb merged 6 commits into
mainfrom
feat/oidc-edge-routes

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What

The API side of ADR 0001: the service-token routes the OpenID Provider service (fells-code/seamless-auth-oidc-provider) calls, and the tables behind them. The API stays private, bearer only and cookie-free. Every /oidc/* route is adapter: false, answers only the provider service's own service token (SEAMLESS_OIDC_SERVICE_TOKEN, separate from the adapters' secret), and answers 404 (the unmatched-route body, checked before the token) while oidc_provider_enabled is off, which is the default.

Routes (contract from fells-code/seamless-auth-types#123)

  • /oidc/payloads/:model/*: upsert, find, by user code, by uid, consume, destroy, revoke by grant. A record tied to a session that is no longer live reads as absent, so logout, admin revoke, sign-out everywhere, sessionsRevokedBefore and chain revocation all end provider access without knowing the provider exists. ReplayDetection inserts only if absent in one statement. expiresIn is capped at 90 days.
  • POST /oidc/sign (D4): decodes the library's signing input and signs with the active OIDC_JWKS_* key only when the session chain is live, the session holds a stored provider Grant for that client, sub, acr, amr and auth_time match what the session earned, identity claims (email, phone_number, roles, org_id and their _verified flags) equal the account's own, no claim falls outside what the library issues for that token kind, the header carries only alg/kid/typ, iss is oidc_issuer, and aud is the client (ID tokens) or a linked resource that is neither ISSUER nor oidc_issuer (access tokens). Refusals are 400/403 with a code and an oidc_sign_failed audit event.
  • GET /oidc/jwks (active key first, plus activeKid), GET /oidc/clients/:id (never a secret), POST /oidc/clients/:id/verify-secret (HMAC lookup, constant time, only for opted-in clients with allow_client_secrets), GET /oidc/accounts/:id, GET /oidc/sessions/:id/auth-context, POST /oidc/sessions/:id/revoke, POST /oidc/events, GET /oidc/config.

Data (D5)

  • Migrations: oidc_payloads (JSONB, PK on model+id, partial indexes), oidc_clients (check constraints for client type and auth method, client_secret_jwt impossible), oidc_resources, oidc_client_resources, and sessions.authMethods/authTime.
  • Sign-in records the method that proved the user (passkey deviceBound from credentialDeviceType, userVerified). Refresh carries it to the rotated row. acr/amr derivation follows D9.
  • The provider follows replacedBySessionId from the recorded session to the current row, so the edge's own adapter refreshes do not break provider access.

Separate edge credential (ADR D2, updated here): the provider service signs its service tokens with SEAMLESS_OIDC_SERVICE_TOKEN. /oidc/* accepts only that token (an adapter's token gets 401). Elsewhere it is honoured only for the trusted client context headers, so the edge's first-party sign-ins see real users, and external delivery refuses it. That closes the path where a compromised edge could request any user's OTP or magic link, sign in as them, and have /oidc/sign sign for them. The server refuses to start if the edge secret equals SEAMLESS_SERVICE_TOKEN, and the flag cannot be enabled without it. The ADR's D2, D13 and security table are updated to match.

Config (D8, D13): new keys oidc_provider_enabled, oidc_issuer, allow_client_secrets, third_party_clients_enabled (env OIDC_*). Enabling is refused, through PATCH and at boot, unless the issuer is https, differs from ISSUER, sits under rpid, the provider keys are present, and none of them reuses a SEAMLESS_JWKS key.

Docs: architecture, configuration, production-operations (provider key rotation) and ecosystem (new Tier 1 dependent). The ADR amendment is in #522.

Contract-affecting: new routes, new system config keys and auth event types. Consumers: seamless-auth-oidc-provider, and the dashboard/CLI for the new config keys.

Draft because

@seamless-auth/types 0.34.0-next.0 is not published yet. package.json pins it and the lock entry points at the registry URL without an integrity hash, so CI goes green once types#123 and its release PR are merged.

Decisions (Brandon, 2026-10-30)

  1. Separate edge secret. Done above. The ADR amendment is its own PR, docs(adr): give the provider service its own token and bind lookups to a grant #522.
  2. Refresh extends the session. Each provider token /oidc/sign signs for a session moves that session's idle expiry forward (lastUsedAt, idleExpiresAt up to session_idle_ttl). It never goes past expiresAt, so the absolute lifetime still bounds provider access. Client credentials touch no session.
  3. Lookups bound to a live grant. GET /oidc/accounts/:id?clientId&sessionId and GET /oidc/sessions/:id/auth-context?clientId answer only for a live session holding a provider Grant for that client, and accounts only for that session's user. Anything else is a 404, and a missing binding is a 400. The edge therefore saves the Grant at sign-in, before it reads the auth context.

Step-up does not record auth methods yet (#510). Admin routes for clients and resources are #407.

Part of #387. Implements the API parts of #405 (sign, store), #509 (resources), #510 (auth context columns and sign-in recording), and lays the table #414 needs.

Checks

  • npm run typecheck, npm run lint, npm run format:check, npm run build, npm run check:changeset-bump: pass
  • npm run coverage: 2536 tests pass, lines 98.99%, branches 95.68% (thresholds 98/95). New services, routes and middleware at 100% lines.
  • TEST_DB=postgres against a throwaway Postgres 16: the migration chain runs, all 47 postgres specs pass including the new tests/postgres/oidcProvider.spec.ts (replay insert-once under concurrency, consumed state, constraints, grant binding). Both new migrations undo and reapply cleanly.
  • An independent security review of the diff. Its findings are fixed here, including all three decisions above.

Comment thread src/services/oidcSigning.ts Fixed
Comment thread src/controllers/oidcEdge.ts Fixed
@Bccorb
Bccorb marked this pull request as ready for review October 30, 2026 22:41
@Bccorb
Bccorb merged commit bb97235 into main Oct 30, 2026
9 checks passed
@Bccorb
Bccorb deleted the feat/oidc-edge-routes branch October 30, 2026 23:19
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.

2 participants