Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/oidc-edge-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'seamless-auth-api': minor
---

Adds the API side of the OpenID Provider service (ADR 0001): service-token routes under `/oidc/*` for fells-code/seamless-auth-oidc-provider, and the tables they use.

- `/oidc/payloads/*` stores every `oidc-provider` model in the new `oidc_payloads` table. A record tied to a session that is no longer live reads as absent, so any revocation ends provider access at once. Replay detection inserts only if absent, in one statement.
- `POST /oidc/sign` signs provider tokens with the separate `OIDC_JWKS_*` key set, and only for a live session holding a provider grant for that client, with the `acr`, `amr` and `auth_time` that session earned, identity claims equal to the account's own, no claims outside what the library issues, an audience the client may use (never the API or the provider itself) and the active key. `GET /oidc/jwks` publishes the public keys. The server refuses provider keys that reuse a first-party signing key.
- `GET /oidc/accounts/:id` and `GET /oidc/sessions/:id/auth-context` take the calling `clientId` (and, for accounts, the `sessionId`) and answer only for a live session holding a provider grant for that client, the same binding `/oidc/sign` applies.
- Each provider token signed for a session counts as activity on it, so an app refreshing its tokens keeps the session's idle expiry moving. The absolute lifetime (`refresh_token_ttl`) still bounds it.
- `GET /oidc/clients/:id`, `POST /oidc/clients/:id/verify-secret`, `GET /oidc/accounts/:id`, `GET /oidc/sessions/:id/auth-context`, `POST /oidc/sessions/:id/revoke`, `POST /oidc/events` and `GET /oidc/config` complete the contract from `@seamless-auth/types` 0.34.0.
- The provider service gets its own service token secret, `SEAMLESS_OIDC_SERVICE_TOKEN`, which must differ from `SEAMLESS_SERVICE_TOKEN` (the server refuses to start otherwise). Every `/oidc/*` route answers only that token, and an adapter's token gets 401. Elsewhere it is honoured only for the trusted client context headers, and external delivery refuses it.
- Every `/oidc/*` route answers 404 while the new `oidc_provider_enabled` system config key is off, which is the default. The server refuses to turn it on, through the admin API or the environment, unless `SEAMLESS_OIDC_SERVICE_TOKEN` is set, `oidc_issuer` is https, differs from `ISSUER`, sits under `rpid`, and the provider keys are present.
- New tables `oidc_payloads`, `oidc_clients`, `oidc_resources` and `oidc_client_resources`, and new `sessions` columns `authMethods` and `authTime`. Sign-in now records the method that proved the user (with the passkey's backup state and user verification), and a refresh carries it forward.
- New system config keys and environment variables: `oidc_provider_enabled` (`OIDC_PROVIDER_ENABLED`), `oidc_issuer` (`OIDC_ISSUER`), `allow_client_secrets` (`OIDC_ALLOW_CLIENT_SECRETS`) and `third_party_clients_enabled` (`OIDC_THIRD_PARTY_CLIENTS_ENABLED`). New `oidc_*` auth event types.

Run the migrations before deploying. Nothing changes for a deployment that leaves the provider off. Requires `@seamless-auth/types` 0.34.0-next.0.
39 changes: 39 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,45 @@ README) but unsupported for cookie-based browser sessions, because this API issu
JSON bodies and expects the caller to hold them securely. Keeping token custody in the adapter is
the recommended path.

### OpenID Provider service

Apps that speak OpenID Connect sign users in through a separate public service,
[seamless-auth-oidc-provider](https://github.com/fells-code/seamless-auth-oidc-provider) (the
"login edge"), not through this API. The design is [ADR 0001](./adr/0001-openid-provider.md).

```mermaid
flowchart LR
rp["Any OIDC app"]
edge["OpenID Provider service<br/>(public: OIDC endpoints, hosted pages, cookies)"]
api["This API<br/>(private, no cookies)"]

rp --> edge
edge -- "first-party sign-in<br/>Bearer + its own service token" --> api
edge -- "/oidc/* provider storage and signing<br/>its own service token only" --> api
```

- The edge signs users in through the same first-party routes every adapter uses, so a provider
sign-in is an ordinary `sessions` row with every first-party protection applied.
- Its `oidc-provider` storage lives here, in `oidc_payloads`, reached through
`/oidc/payloads/*`. A record tied to a session that is no longer live reads as absent, so any
revocation ends provider access at once.
- Account claims and auth context answer only for a live session holding a provider grant for
the calling client, the same binding signing uses. Each provider token signed for a session
counts as activity on it, so an app refreshing its tokens keeps the session alive up to its
absolute lifetime.
- Provider signing keys (`OIDC_JWKS_*`) stay here. `POST /oidc/sign` signs only for a user with a
live session, with the `acr`, `amr` and `auth_time` that session earned, for an audience the
client may use, and only with the active key.
- The edge has its own service token, signed with `SEAMLESS_OIDC_SERVICE_TOKEN` rather than the
adapters' secret. `/oidc/*` accepts only that token. Elsewhere it is honoured only for the
trusted client context headers, and external delivery refuses it, so a compromised edge cannot
receive anyone's codes or magic links.
- `/oidc/*` answers 404 while `oidc_provider_enabled` is off. These routes are not the public
OIDC endpoints.

Provider tokens carry `iss` equal to `oidc_issuer`, never `ISSUER`, and are signed with provider
keys, so the first-party verifier rejects them.

For the full dependency and contract-coupling map across sibling repositories, see
[ecosystem.md](./ecosystem.md).

Expand Down
20 changes: 20 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,26 @@ Required when `NODE_ENV=production`. In development, signing keys are generated

See [docs/production-operations.md](./production-operations.md) for key rotation.

### OpenID Provider

The API's side of the OpenID Provider service ([ADR 0001](./adr/0001-openid-provider.md)). All of
it is off until a deployment runs the provider service and turns it on.

| Variable | Required | Default | Notes |
| ---------------------------------- | ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `OIDC_PROVIDER_ENABLED` | No | `false` | Seeds `oidc_provider_enabled`. While off, every `/oidc/*` route answers 404. The server refuses to start with it on unless the checks below pass. |
| `OIDC_ISSUER` | When enabled | - | Seeds `oidc_issuer`, the provider service's public URL. Must be https, must differ from `ISSUER`, and its host must be `rpid` or a subdomain of it. |
| `OIDC_ALLOW_CLIENT_SECRETS` | No | `false` | Seeds `allow_client_secrets`. Lets an admin opt individual provider clients in to shared secrets. `private_key_jwt` is the default without it. |
| `OIDC_THIRD_PARTY_CLIENTS_ENABLED` | No | `false` | Seeds `third_party_clients_enabled`. Third-party clients always show a consent page. |
| `SEAMLESS_OIDC_SERVICE_TOKEN` | When enabled | - | The provider service's own service token secret. Must differ from `SEAMLESS_SERVICE_TOKEN`, or the server refuses to start. `/oidc/*` accepts only this, and external delivery refuses it. |
| `OIDC_JWKS_ACTIVE_KID` | When enabled | - | Key ID of the provider signing key. Only this key signs. |
| `OIDC_JWKS_KEYS` | When enabled | - | JSON JWK set of private RS256 keys, each with a unique `kid`. Separate from the `SEAMLESS_JWKS_*` keys. Every key in it is published at `/oidc/jwks`. |

The issuer has to sit under `rpid` because the hosted sign-in pages run on it and passkeys are
bound to `rpid`. Changing `rpid` to fit a new hostname orphans every existing passkey, so pick
the provider hostname to fit the `rpid` you have (for example `login.example.com` under
`example.com`).

### Development signing keys

Outside production the server creates an RSA key pair at startup, before it starts listening, so
Expand Down
12 changes: 12 additions & 0 deletions docs/ecosystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ Provider-agnostic email/SMS adapter contract. **Reverse coupling.**
`send()` signature, package renames, new required adapter config, or removing
`MessagingConfigurationError`.

### `seamless-auth-oidc-provider`, the OpenID Provider service

The public login edge for apps that speak OpenID Connect ([ADR 0001](./adr/0001-openid-provider.md)).
It builds on `@seamless-auth/express` for first-party sign-in and calls the
`/oidc/*` routes with its own service token (`SEAMLESS_OIDC_SERVICE_TOKEN`) for provider storage, signing, client metadata, claims and auth context.

- Contract: the `oidcEdge` module of `@seamless-auth/types` (`OidcEdgeRoutes`,
`OIDC_EDGE_CONTRACT_VERSION`). The edge refuses to start against an API on another contract
version, or one that answers 404 because `oidc_provider_enabled` is off.
- **Breaks if changed here:** any `/oidc/*` route, request or response shape, the `/oidc/sign`
refusal codes, the `acr`/`amr` derivation, or the service-token check.

## Tier 2 — downstream consumers (transitively affected)

| Repo | What it is | Coupling |
Expand Down
16 changes: 16 additions & 0 deletions docs/production-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ Production deployments should define:
- `SEAMLESS_JWKS_ACTIVE_KID`
- `SEAMLESS_JWKS_KEY_<kid>_PRIVATE`
- `SEAMLESS_JWKS_PUBLIC_KEYS`
- `SEAMLESS_OIDC_SERVICE_TOKEN`, `OIDC_JWKS_ACTIVE_KID` and `OIDC_JWKS_KEYS`, when the OpenID
Provider is enabled. Give the provider service `SEAMLESS_OIDC_SERVICE_TOKEN` only, never the
adapters' `SEAMLESS_SERVICE_TOKEN`.
- OAuth client-secret environment variables referenced by provider `clientSecretEnv`
- Messaging provider credentials when direct delivery is enabled

Expand Down Expand Up @@ -157,6 +160,19 @@ selecting the signer, and the three steps above are three applies. See ADR 0011,
`Terraform owns the JWKS document, and rotation overlaps`, and the procedure in that repo's
`portal-auth/README.md`.

### OpenID Provider keys

The provider has its own key set, `OIDC_JWKS_KEYS`, so a provider token can never verify as a
first-party one. Every key in the set is published at `/oidc/jwks`, which the provider service
serves as its public `/jwks`, and only `OIDC_JWKS_ACTIVE_KID` signs. Rotation follows the same
three steps:

1. **Add.** Add the new private JWK to `OIDC_JWKS_KEYS` and deploy. It is published, and the
outgoing key still signs. Wait until relying parties have refreshed their cached JWKS.
2. **Flip.** Point `OIDC_JWKS_ACTIVE_KID` at the new key and deploy.
3. **Retire.** Once the longest provider token lifetime has passed, remove the outgoing key and
deploy.

## Secrets Separate From The Service Token

`REFRESH_TOKEN_LOOKUP_SECRET`, `TOTP_SECRET_ENCRYPTION_KEY`, `OAUTH_STATE_SECRET` and
Expand Down
Loading
Loading