Repository navigation
feat: add the /oidc/* service routes and tables for the OpenID Provider service - #521
Merged
Merged
Conversation
Bccorb
marked this pull request as ready for review
October 30, 2026 22:41
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 isadapter: 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) whileoidc_provider_enabledis 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,sessionsRevokedBeforeand chain revocation all end provider access without knowing the provider exists.ReplayDetectioninserts only if absent in one statement.expiresInis capped at 90 days.POST /oidc/sign(D4): decodes the library's signing input and signs with the activeOIDC_JWKS_*key only when the session chain is live, the session holds a stored provider Grant for that client,sub,acr,amrandauth_timematch what the session earned, identity claims (email,phone_number,roles,org_idand their_verifiedflags) equal the account's own, no claim falls outside what the library issues for that token kind, the header carries onlyalg/kid/typ,issisoidc_issuer, andaudis the client (ID tokens) or a linked resource that is neitherISSUERnoroidc_issuer(access tokens). Refusals are 400/403 with a code and anoidc_sign_failedaudit event.GET /oidc/jwks(active key first, plusactiveKid),GET /oidc/clients/:id(never a secret),POST /oidc/clients/:id/verify-secret(HMAC lookup, constant time, only for opted-in clients withallow_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)
oidc_payloads(JSONB, PK on model+id, partial indexes),oidc_clients(check constraints for client type and auth method,client_secret_jwtimpossible),oidc_resources,oidc_client_resources, andsessions.authMethods/authTime.deviceBoundfromcredentialDeviceType,userVerified). Refresh carries it to the rotated row.acr/amrderivation follows D9.replacedBySessionIdfrom 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/signsign for them. The server refuses to start if the edge secret equalsSEAMLESS_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(envOIDC_*). Enabling is refused, through PATCH and at boot, unless the issuer is https, differs fromISSUER, sits underrpid, the provider keys are present, and none of them reuses aSEAMLESS_JWKSkey.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/types0.34.0-next.0 is not published yet.package.jsonpins 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)
/oidc/signsigns for a session moves that session's idle expiry forward (lastUsedAt,idleExpiresAtup tosession_idle_ttl). It never goes pastexpiresAt, so the absolute lifetime still bounds provider access. Client credentials touch no session.GET /oidc/accounts/:id?clientId&sessionIdandGET /oidc/sessions/:id/auth-context?clientIdanswer 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: passnpm run coverage: 2536 tests pass, lines 98.99%, branches 95.68% (thresholds 98/95). New services, routes and middleware at 100% lines.TEST_DB=postgresagainst a throwaway Postgres 16: the migration chain runs, all 47 postgres specs pass including the newtests/postgres/oidcProvider.spec.ts(replay insert-once under concurrency, consumed state, constraints, grant binding). Both new migrations undo and reapply cleanly.