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
5 changes: 5 additions & 0 deletions .changeset/public-oidc-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'seamless-auth-api': minor
---

`GET /system-config/public` can now tell a native admin tool how to sign in through the OpenID Provider (ADR 0003). A new system config key, `oidc_cli_client_id` (seeded from `OIDC_CLI_CLIENT_ID`, or set through `PATCH /system-config/admin`), names the application. The response gains an optional `oidcProvider: { issuer, cliClientId }`, present only when the provider is on, the issuer and key are set, and that application is active and eligible for session exchange with it turned on. Setting the key to an unknown or ineligible application answers 400; later disabling the application or turning its exchange off makes the advertisement disappear. Also fixes `PATCH /system-config/admin` answering 500 when it set a nullable key such as `oidc_issuer`, `hosted_ui` or `max_concurrent_sessions` to `null`: the row is now removed, which reads back as `null`. No migration is needed. Needs the `@seamless-auth/types` release that adds these fields.
2 changes: 2 additions & 0 deletions docs/admin-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ the API log.

The applications (clients) of the OpenID Provider service ([ADR 0001](./adr/0001-openid-provider.md), D12) are managed under `/admin/applications`. Reads need `admin:read`. Every write needs `admin:write` and a fresh step-up, and writes an `admin_oidc_application_*` auth event. The routes work whether or not `oidc_provider_enabled` is on, so applications can be set up before the provider service is deployed.

To offer provider sign-in to a native admin tool, turn `sessionExchangeEnabled` on for its application, then name it with `oidc_cli_client_id` through `PATCH /system-config/admin`. `GET /system-config/public` advertises it while the provider is on and the application stays eligible ([ADR 0003](./adr/0003-native-admin-access.md)).

| Route | What it does |
| ------------------------------------- | -------------------------------------------------------------------------- |
| `GET /admin/applications` | List applications. |
Expand Down
38 changes: 24 additions & 14 deletions docs/adr/0003-native-admin-access.md

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions docs/api-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ A first-party public native client signed in through the OpenID Provider reaches
for the client, with `PATCH /admin/applications/:id`. It is off by default, and accepted only on a
first-party public client with a loopback or private-use redirect URI.

**Finding the provider.** An admin names the application with the `oidc_cli_client_id` system
config key, through `PATCH /system-config/admin`. `GET /system-config/public` then answers with:

```json
{
"loginMethods": ["passkey", "email_otp"],
"recoveryCodes": true,
"accountRecovery": false,
"oidcProvider": { "issuer": "https://login.example.com", "cliClientId": "seamless-cli" }
}
```

`oidcProvider` is sent whole or not at all. It is present only when `oidc_provider_enabled` is on,
`oidc_issuer` and `oidc_cli_client_id` are set, and that application exists, is active and is
eligible for exchange with it turned on. An API that predates the field does not send it either,
so a client must read its absence as "this instance does not advertise" and offer email code
sign-in. Endpoints come from the OIDC discovery document at
`<issuer>/.well-known/openid-configuration`, not from this response. The route is
`Cache-Control: no-store`.

The client runs authorization code with PKCE, or the device grant, with
`resource=<oidc_issuer>/seamless` and `scope=openid session:exchange`, and without
`offline_access`. It then has 60 seconds to send the access token here, directly rather than
Expand Down Expand Up @@ -179,6 +199,11 @@ recovers: `/totp/enroll/*`, `/totp/disable`, `/webauthn/register/*`, `POST /reco
stay open: they gain no access, though they could slow the owner's response. As for any session, a
refresh does not carry a step-up to the new row.

A session from an authorization code grant steps up with a passkey by signing in again with
`acr_values=phr` and `max_age=0`, then exchanging again and logging out the old session. With no
browser session, that records a sign-in and not a step-up, so the command has to run a second
time. It can also use `POST /totp/verify-mfa`.

**A device sign-in with no TOTP cannot enroll one.** The user enrolls from a browser, then signs in
on the device again. Without this, someone who phished a device approval could enroll their own
authenticator and step up with it. A session from an authorization code grant, and every other
Expand Down
22 changes: 12 additions & 10 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,17 @@ See [docs/production-operations.md](./production-operations.md) for key rotation
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. While it is off, existing third-party clients are inactive: the API does not serve them to the edge, verify their secrets, sign for them or accept their notice tokens. |
| `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`. |
| `OIDC_HOSTED_UI` | No | - | Seeds `hosted_ui` as JSON: branding for the hosted pages (`appName`, an https `logoUrl`, hex `colors`, `fontFamily`). Admins change it through `PATCH /system-config/admin`. |
| 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_CLI_CLIENT_ID` | No | - | Seeds `oidc_cli_client_id`, the application a native admin tool signs in with, advertised on `GET /system-config/public` ([ADR 0003](./adr/0003-native-admin-access.md)). The application has to be active and eligible for session exchange, or nothing is advertised. Admins change it through `PATCH /system-config/admin`, which refuses an ineligible one. An admin clearing it while this variable is set gets it back at the next start. |
| `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. While it is off, existing third-party clients are inactive: the API does not serve them to the edge, verify their secrets, sign for them or accept their notice tokens. |
| `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`. |
| `OIDC_HOSTED_UI` | No | - | Seeds `hosted_ui` as JSON: branding for the hosted pages (`appName`, an https `logoUrl`, hex `colors`, `fontFamily`). Admins change it through `PATCH /system-config/admin`. |

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
Expand Down Expand Up @@ -469,6 +470,7 @@ Validation is enforced by [`systemConfig.schema.ts`](../src/schemas/systemConfig
| `origins` | url[] | `ORIGINS` | - |
| `frontend_url` | url | `FRONTEND_URL` | - |
| `magic_link_redirect_uris` | string[] | `MAGIC_LINK_REDIRECT_URIS` | `[]` |
| `oidc_cli_client_id` | string or null | `OIDC_CLI_CLIENT_ID` | `null` |

### Flow rate limits

Expand Down
Loading
Loading