From 01850630f1f92a939ad3ceade6c1908881296758 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:08:00 +0000 Subject: [PATCH] chore: update version and changelog --- .../backchannel-logout-one-application.md | 5 ---- .changeset/device-code-expired-token.md | 5 ---- .changeset/external-delivery-subject.md | 5 ---- ...n-secret-responses-and-provider-fetches.md | 5 ---- .changeset/notice-limit-canonical-user.md | 5 ---- .changeset/notice-text-without-links.md | 5 ---- .changeset/oauth-start-failure-detail.md | 5 ---- .changeset/oidc-client-links.md | 5 ---- .changeset/organization-roles-in-token.md | 5 ---- .../passkey-assurance-from-registration.md | 5 ---- .changeset/permissions-mapped-to-roles.md | 5 ---- .changeset/public-oidc-provider.md | 5 ---- .changeset/send-unadvertised-notice-kinds.md | 5 ---- .changeset/session-exchange.md | 7 ----- .changeset/system-config-key-order.md | 5 ---- .changeset/third-party-clients-gate.md | 5 ---- CHANGELOG.md | 27 +++++++++++++++++++ adapter-manifest.json | 2 +- openapi.json | 2 +- package.json | 2 +- 20 files changed, 30 insertions(+), 85 deletions(-) delete mode 100644 .changeset/backchannel-logout-one-application.md delete mode 100644 .changeset/device-code-expired-token.md delete mode 100644 .changeset/external-delivery-subject.md delete mode 100644 .changeset/harden-secret-responses-and-provider-fetches.md delete mode 100644 .changeset/notice-limit-canonical-user.md delete mode 100644 .changeset/notice-text-without-links.md delete mode 100644 .changeset/oauth-start-failure-detail.md delete mode 100644 .changeset/oidc-client-links.md delete mode 100644 .changeset/organization-roles-in-token.md delete mode 100644 .changeset/passkey-assurance-from-registration.md delete mode 100644 .changeset/permissions-mapped-to-roles.md delete mode 100644 .changeset/public-oidc-provider.md delete mode 100644 .changeset/send-unadvertised-notice-kinds.md delete mode 100644 .changeset/session-exchange.md delete mode 100644 .changeset/system-config-key-order.md delete mode 100644 .changeset/third-party-clients-gate.md diff --git a/.changeset/backchannel-logout-one-application.md b/.changeset/backchannel-logout-one-application.md deleted file mode 100644 index cf3d3189..00000000 --- a/.changeset/backchannel-logout-one-application.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -Back-channel logout now reaches an application whose grant ends while the first-party session goes on (#543). When the provider service deletes a `Grant` (the user chooses "only sign out of this app", or the grant is revoked), the API sends that application a logout token for the `sid` it was given, with the same sender and the same `oidc_backchannel_logout_sent` and `oidc_backchannel_logout_failed` events as a session logout. Other applications on the session are not told, and nothing is sent while another live grant still signs the application in on that session. Applications that register a `backchannel_logout_uri` should expect a logout token for a partial logout as well as a full one. diff --git a/.changeset/device-code-expired-token.md b/.changeset/device-code-expired-token.md deleted file mode 100644 index 633f5bd8..00000000 --- a/.changeset/device-code-expired-token.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -`GET /oidc/payloads/DeviceCode/:id` now returns a device code for 10 minutes after it expires, with its `exp`, instead of 404, and the purge job keeps it that long (#542). The provider service hands that record to its library, which answers a client polling with an expired code `expired_token` (RFC 8628 section 3.5) rather than `invalid_grant`. A code that never existed, one past that window, the by-user-code lookup and every other model still answer 404 once expired, and an expired code can never be consumed. diff --git a/.changeset/external-delivery-subject.md b/.changeset/external-delivery-subject.md deleted file mode 100644 index 74ba3557..00000000 --- a/.changeset/external-delivery-subject.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -External delivery now requires the service token in `x-seamless-service-token` to carry subject `seamless-auth-external-delivery` (#556). Any other valid service token, such as an adapter's proxy token, no longer unlocks codes, links or account notices in the response; the API sends them through its own transport instead. The Node, Go, Python and Rust adapters already sign their delivery requests with that subject, so adopters on current adapters need no change. A caller that hand-mints its own service token for external delivery must set `sub` to `seamless-auth-external-delivery`. diff --git a/.changeset/harden-secret-responses-and-provider-fetches.md b/.changeset/harden-secret-responses-and-provider-fetches.md deleted file mode 100644 index 78e2f65a..00000000 --- a/.changeset/harden-secret-responses-and-provider-fetches.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': patch ---- - -Two hardening fixes from #556. Every API response now carries `Cache-Control: no-store` unless its route sets its own caching (the JWKS and the admin dashboard's hashed assets do), so tokens, client secrets from create and rotate, recovery codes and delivery payloads are never stored by a cache. Provider requests (OAuth token, userinfo and discovery, and back-channel logout) now go through the address guard in every mode: outside production, `http` and private or loopback addresses are still allowed, but link-local `169.254.0.0/16` and unspecified `0.0.0.0/8` addresses are refused whether named directly or reached through DNS. diff --git a/.changeset/notice-limit-canonical-user.md b/.changeset/notice-limit-canonical-user.md deleted file mode 100644 index d54cefd0..00000000 --- a/.changeset/notice-limit-canonical-user.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': patch ---- - -The per-user limit on `POST /applications/account-notices` now folds the `userId` to lower case before counting (#553). Upper-case variants of one UUID no longer each get their own allowance of 10 notices an hour. diff --git a/.changeset/notice-text-without-links.md b/.changeset/notice-text-without-links.md deleted file mode 100644 index 684f065a..00000000 --- a/.changeset/notice-text-without-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -Application notice text can no longer carry its own links (#554). `POST /applications/account-notices` refuses with `400` a `text` that contains a URL, a `www.` address, a domain name or an email address, since mail and SMS clients turn all of them into links; `actionUrl`, checked against the application's origins, is the only link a notice carries. The `Sent by through .` attribution is now the first line of the message, ahead of the text, rather than a footer the text could imitate. Adapters that render notices from external delivery receive the new layout in `text`. diff --git a/.changeset/oauth-start-failure-detail.md b/.changeset/oauth-start-failure-detail.md deleted file mode 100644 index 47098fe6..00000000 --- a/.changeset/oauth-start-failure-detail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': patch ---- - -A failed OAuth start now says why. The `oauth_login_failed` event with reason `start_failed` carries a `detail` of `redirect_not_allowed` or `provider_unusable`, and the error is logged. A refused redirect is logged by scheme and host only. The answer to the client is unchanged. diff --git a/.changeset/oidc-client-links.md b/.changeset/oidc-client-links.md deleted file mode 100644 index 92cca617..00000000 --- a/.changeset/oidc-client-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -A confidential OpenID Provider application can send account notices for the public applications it acts for (ADR 0002, N4 and N5). An admin links them with `PUT /admin/applications/:id/links/:linkedId`, lists links with `GET /admin/applications/:id/links` and removes one with `DELETE`, each write with step-up and audited as `admin_oidc_application_link_added` or `admin_oidc_application_link_removed`. A link needs a confidential acting application and a different public linked one, both first-party or both third-party. `POST /applications/account-notices` takes an optional `clientId`: the caller must be linked to it, the grant check uses it and the template names it, and a client the caller is not linked to answers the same `404 notice_unknown_user` as an unknown user. Applications gain `noticeActionOrigins`, bare `https` origins that a notice's action link may point at besides the `https` redirect origins, and every application response lists `noticeActionOrigins`, `links` and `linkedFrom`. A new migration adds the `oidc_client_links` table and the column. ADR 0001 D14 is amended to match. diff --git a/.changeset/organization-roles-in-token.md b/.changeset/organization-roles-in-token.md deleted file mode 100644 index 9912fbb7..00000000 --- a/.changeset/organization-roles-in-token.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -A backend can authorize per organization from the access token alone (#416, ADR 0004). Inside an active organization, tokens gain `org_roles` and `org_permissions` beside `org_id`, and `GET /users/me` and `GET /users/me/permissions` report the same. Organization roles never appear in `roles`, so a guard on `admin` does not pass for an organization admin. Organization roles are now a catalogue, managed with `POST /admin/organization-roles` and `PATCH` and `DELETE /admin/organization-roles/:roleName`, listed by `GET /admin/roles`, and mapped to permissions with `PUT /admin/roles/organization/:roleName/permissions`. A new migration seeds it with the built-in `owner`, `admin` and `member` and every value memberships already hold, and rewrites no membership. **Behavior changes:** assigning a role outside the catalogue now answers 400. `PATCH /organizations/:id/members/:userId` now needs step-up. Only an owner or an instance `admin:write` can grant or remove `owner`, or remove an owner from an organization, and any other manager can only grant a role whose permissions they hold, so an organization admin can no longer make themselves owner. Adding, changing and removing members is audited as `organization_member_added`, `organization_member_updated` and `organization_member_removed`, and a refused change as `organization_member_role_grant_refused`. **Fixes:** removing a member now reaches the token. Previously every refresh copied `org_id` from the session until it ended. Now the next token drops the organization claims and the session stops naming the organization. Recovery tokens carry no organization claims. diff --git a/.changeset/passkey-assurance-from-registration.md b/.changeset/passkey-assurance-from-registration.md deleted file mode 100644 index 04ac0705..00000000 --- a/.changeset/passkey-assurance-from-registration.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -Passkey hardware assurance now comes from the registered credential rather than the assertion (#552). Sign-in and step-up refuse an assertion whose backup eligibility flag differs from the one stored at registration (`401`, audited as `webauthn_login_failed` or `step_up_failed` with reason `Backup eligibility differs from registration`). A passkey counts as device-bound, and earns `acr=phrh` and `amr=hwk`, only when it was registered device-bound with attestation verified against the FIDO Metadata Service. Deployments that use the default `none` attestation, and credentials registered before attestation was recorded, now get `phr` and `swk` instead of `phrh` and `hwk`. diff --git a/.changeset/permissions-mapped-to-roles.md b/.changeset/permissions-mapped-to-roles.md deleted file mode 100644 index 277596d4..00000000 --- a/.changeset/permissions-mapped-to-roles.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -Roles can now grant permissions, and access tokens carry them (#415, ADR 0004). An admin manages the catalogue with `GET` and `POST /admin/permissions` and `PATCH` and `DELETE /admin/permissions/:permissionName`, reads roles with `GET /admin/roles`, and replaces what a role grants with `PUT /admin/roles/:kind/:roleName/permissions`. Each write needs step-up and is audited. Deleting a permission a role still grants answers 409 with those roles. Instance roles stay in `available_roles`, and removing one also removes its mappings, so adding the name back later does not restore old grants. Access tokens gain a `permissions` claim, matched exactly with no wildcards or implied grants, which is empty on a recovery session. When the permission claims would pass 1 KiB, which keeps an adapter's session cookie under the browser limit, the token instead carries `permissions_omitted: true` and a guard reads `GET /users/me/permissions`, which answers from the database on every call. `GET /users/me` gains `user.permissions`. Changes reach a token when it is next issued, within the access token lifetime. New migration adds the `permissions` and `role_permissions` tables. diff --git a/.changeset/public-oidc-provider.md b/.changeset/public-oidc-provider.md deleted file mode 100644 index e49ff9c0..00000000 --- a/.changeset/public-oidc-provider.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'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. diff --git a/.changeset/send-unadvertised-notice-kinds.md b/.changeset/send-unadvertised-notice-kinds.md deleted file mode 100644 index 59bb9e7b..00000000 --- a/.changeset/send-unadvertised-notice-kinds.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -Owner notices that a delivery-owning adapter does not advertise are now sent directly instead of dropped (#557). Under external delivery with a service token, a notice whose kind is missing from `x-seamless-auth-delivery-kinds` (for example the SMS notice on `/recovery/verify` when the adapter lists only `account_notice_email`) goes out through the API's own email or SMS transport when one is configured, and is recorded as `sent` or `failed` rather than `not_sent_external_delivery`. The same applies to the recovery completed and recovery code used notices and to `POST /applications/account-notices`. A recovery that previously answered `503 recovery_notice_failed` for an adapter listing no notice kinds now proceeds when the API can notify the owner itself. Admin identity-change notices are unchanged. diff --git a/.changeset/session-exchange.md b/.changeset/session-exchange.md deleted file mode 100644 index 18a5bec9..00000000 --- a/.changeset/session-exchange.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -A first-party native client signed in through the OpenID Provider can now reach the admin API, by exchanging the sign-in for a first-party session of its own (ADR 0003). An admin turns `sessionExchangeEnabled` on for a first-party public native application, with step-up, audited as `admin_oidc_application_session_exchange_updated`. Such a client can ask for a 60 second access token for `/seamless` with the single scope `session:exchange`, and `/oidc/sign` records each one. `POST /sessions/exchange` (`adapter: false`, RFC 8693 parameter names) consumes that token once and returns the `/refresh` body plus `issued_token_type`, for a new session linked to the provider sign-in. A second presentation answers `400 invalid_request` with `token_already_used` and revokes the session the first one created, audited as `session_exchange_replayed`. A successful exchange is audited as `session_exchanged`. - -The new session has its own refresh chain and step-up state, ends whenever its parent ends, never outlives it, and refreshing it counts as activity on the parent. It copies the parent's auth context and step-up for an authorization code grant, and nothing for a device grant. A device-grant session answers `403 step_up_required` until it has a fresh step-up, on every route behind `requireAdmin` and every `/organizations` route except `POST /organizations/:id/switch`, reads included, and on `/totp/enroll/*`, `/totp/disable`, `/webauthn/register/*`, `POST /recovery-codes`, `DELETE /users/credentials`, `/registration/phone`, `/registration/phone/verify` and `DELETE /recovery/requests/:id`, so a device sign-in with no TOTP cannot enroll one and the user does it from a browser. It can step up with TOTP or a real passkey assertion on the relying party's domain. OpenAPI now documents this 403 on those routes and on admin reads. It does not count against `max_concurrent_sessions`. Disabling or deleting the application, or turning the flag off, revokes the sessions it exchanged for. Session responses gain `parentSessionId`, `exchangeClientId` and `exchangeGrantType` on such a session, and application responses gain `sessionExchangeEnabled`. A new migration adds the columns. ADR 0001 D11 is amended to match. diff --git a/.changeset/system-config-key-order.md b/.changeset/system-config-key-order.md deleted file mode 100644 index e8269ac4..00000000 --- a/.changeset/system-config-key-order.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': patch ---- - -Starting the server no longer rewrites an env-backed `system_config` row, or warns that an admin change is being reverted, when the stored value only orders its keys differently. Postgres JSONB does not keep key order, so object values such as `lockout_policy` and `flow_rate_limits` looked changed on every boot. diff --git a/.changeset/third-party-clients-gate.md b/.changeset/third-party-clients-gate.md deleted file mode 100644 index f58b58aa..00000000 --- a/.changeset/third-party-clients-gate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'seamless-auth-api': minor ---- - -`third_party_clients_enabled` now gates existing third-party OpenID Provider clients, not only creating and updating them (#555). While it is off, a client that is not first-party is treated like a disabled one: `/oidc/clients/:id` answers `404`, its shared secret does not verify, `/oidc/sign` refuses with `client_inactive`, its notice tokens are refused and back-channel logout skips it. An instance that turned the flag off after registering third-party clients stops serving them on upgrade; turn the flag back on, or mark the applications first-party, to keep them working. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8372370a..af98aec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # seamless-auth-api +## 0.23.0 + +### Minor Changes + +- bcab4d1: Back-channel logout now reaches an application whose grant ends while the first-party session goes on (#543). When the provider service deletes a `Grant` (the user chooses "only sign out of this app", or the grant is revoked), the API sends that application a logout token for the `sid` it was given, with the same sender and the same `oidc_backchannel_logout_sent` and `oidc_backchannel_logout_failed` events as a session logout. Other applications on the session are not told, and nothing is sent while another live grant still signs the application in on that session. Applications that register a `backchannel_logout_uri` should expect a logout token for a partial logout as well as a full one. +- bcab4d1: `GET /oidc/payloads/DeviceCode/:id` now returns a device code for 10 minutes after it expires, with its `exp`, instead of 404, and the purge job keeps it that long (#542). The provider service hands that record to its library, which answers a client polling with an expired code `expired_token` (RFC 8628 section 3.5) rather than `invalid_grant`. A code that never existed, one past that window, the by-user-code lookup and every other model still answer 404 once expired, and an expired code can never be consumed. +- bcab4d1: External delivery now requires the service token in `x-seamless-service-token` to carry subject `seamless-auth-external-delivery` (#556). Any other valid service token, such as an adapter's proxy token, no longer unlocks codes, links or account notices in the response; the API sends them through its own transport instead. The Node, Go, Python and Rust adapters already sign their delivery requests with that subject, so adopters on current adapters need no change. A caller that hand-mints its own service token for external delivery must set `sub` to `seamless-auth-external-delivery`. +- 25c4cc6: Application notice text can no longer carry its own links (#554). `POST /applications/account-notices` refuses with `400` a `text` that contains a URL, a `www.` address, a domain name or an email address, since mail and SMS clients turn all of them into links; `actionUrl`, checked against the application's origins, is the only link a notice carries. The `Sent by through .` attribution is now the first line of the message, ahead of the text, rather than a footer the text could imitate. Adapters that render notices from external delivery receive the new layout in `text`. +- f82ec0c: A confidential OpenID Provider application can send account notices for the public applications it acts for (ADR 0002, N4 and N5). An admin links them with `PUT /admin/applications/:id/links/:linkedId`, lists links with `GET /admin/applications/:id/links` and removes one with `DELETE`, each write with step-up and audited as `admin_oidc_application_link_added` or `admin_oidc_application_link_removed`. A link needs a confidential acting application and a different public linked one, both first-party or both third-party. `POST /applications/account-notices` takes an optional `clientId`: the caller must be linked to it, the grant check uses it and the template names it, and a client the caller is not linked to answers the same `404 notice_unknown_user` as an unknown user. Applications gain `noticeActionOrigins`, bare `https` origins that a notice's action link may point at besides the `https` redirect origins, and every application response lists `noticeActionOrigins`, `links` and `linkedFrom`. A new migration adds the `oidc_client_links` table and the column. ADR 0001 D14 is amended to match. +- bed4994: A backend can authorize per organization from the access token alone (#416, ADR 0004). Inside an active organization, tokens gain `org_roles` and `org_permissions` beside `org_id`, and `GET /users/me` and `GET /users/me/permissions` report the same. Organization roles never appear in `roles`, so a guard on `admin` does not pass for an organization admin. Organization roles are now a catalogue, managed with `POST /admin/organization-roles` and `PATCH` and `DELETE /admin/organization-roles/:roleName`, listed by `GET /admin/roles`, and mapped to permissions with `PUT /admin/roles/organization/:roleName/permissions`. A new migration seeds it with the built-in `owner`, `admin` and `member` and every value memberships already hold, and rewrites no membership. **Behavior changes:** assigning a role outside the catalogue now answers 400. `PATCH /organizations/:id/members/:userId` now needs step-up. Only an owner or an instance `admin:write` can grant or remove `owner`, or remove an owner from an organization, and any other manager can only grant a role whose permissions they hold, so an organization admin can no longer make themselves owner. Adding, changing and removing members is audited as `organization_member_added`, `organization_member_updated` and `organization_member_removed`, and a refused change as `organization_member_role_grant_refused`. **Fixes:** removing a member now reaches the token. Previously every refresh copied `org_id` from the session until it ended. Now the next token drops the organization claims and the session stops naming the organization. Recovery tokens carry no organization claims. +- 25c4cc6: Passkey hardware assurance now comes from the registered credential rather than the assertion (#552). Sign-in and step-up refuse an assertion whose backup eligibility flag differs from the one stored at registration (`401`, audited as `webauthn_login_failed` or `step_up_failed` with reason `Backup eligibility differs from registration`). A passkey counts as device-bound, and earns `acr=phrh` and `amr=hwk`, only when it was registered device-bound with attestation verified against the FIDO Metadata Service. Deployments that use the default `none` attestation, and credentials registered before attestation was recorded, now get `phr` and `swk` instead of `phrh` and `hwk`. +- da66738: Roles can now grant permissions, and access tokens carry them (#415, ADR 0004). An admin manages the catalogue with `GET` and `POST /admin/permissions` and `PATCH` and `DELETE /admin/permissions/:permissionName`, reads roles with `GET /admin/roles`, and replaces what a role grants with `PUT /admin/roles/:kind/:roleName/permissions`. Each write needs step-up and is audited. Deleting a permission a role still grants answers 409 with those roles. Instance roles stay in `available_roles`, and removing one also removes its mappings, so adding the name back later does not restore old grants. Access tokens gain a `permissions` claim, matched exactly with no wildcards or implied grants, which is empty on a recovery session. When the permission claims would pass 1 KiB, which keeps an adapter's session cookie under the browser limit, the token instead carries `permissions_omitted: true` and a guard reads `GET /users/me/permissions`, which answers from the database on every call. `GET /users/me` gains `user.permissions`. Changes reach a token when it is next issued, within the access token lifetime. New migration adds the `permissions` and `role_permissions` tables. +- 2f1c3db: `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. +- 25c4cc6: Owner notices that a delivery-owning adapter does not advertise are now sent directly instead of dropped (#557). Under external delivery with a service token, a notice whose kind is missing from `x-seamless-auth-delivery-kinds` (for example the SMS notice on `/recovery/verify` when the adapter lists only `account_notice_email`) goes out through the API's own email or SMS transport when one is configured, and is recorded as `sent` or `failed` rather than `not_sent_external_delivery`. The same applies to the recovery completed and recovery code used notices and to `POST /applications/account-notices`. A recovery that previously answered `503 recovery_notice_failed` for an adapter listing no notice kinds now proceeds when the API can notify the owner itself. Admin identity-change notices are unchanged. +- 52ff3c3: A first-party native client signed in through the OpenID Provider can now reach the admin API, by exchanging the sign-in for a first-party session of its own (ADR 0003). An admin turns `sessionExchangeEnabled` on for a first-party public native application, with step-up, audited as `admin_oidc_application_session_exchange_updated`. Such a client can ask for a 60 second access token for `/seamless` with the single scope `session:exchange`, and `/oidc/sign` records each one. `POST /sessions/exchange` (`adapter: false`, RFC 8693 parameter names) consumes that token once and returns the `/refresh` body plus `issued_token_type`, for a new session linked to the provider sign-in. A second presentation answers `400 invalid_request` with `token_already_used` and revokes the session the first one created, audited as `session_exchange_replayed`. A successful exchange is audited as `session_exchanged`. + + The new session has its own refresh chain and step-up state, ends whenever its parent ends, never outlives it, and refreshing it counts as activity on the parent. It copies the parent's auth context and step-up for an authorization code grant, and nothing for a device grant. A device-grant session answers `403 step_up_required` until it has a fresh step-up, on every route behind `requireAdmin` and every `/organizations` route except `POST /organizations/:id/switch`, reads included, and on `/totp/enroll/*`, `/totp/disable`, `/webauthn/register/*`, `POST /recovery-codes`, `DELETE /users/credentials`, `/registration/phone`, `/registration/phone/verify` and `DELETE /recovery/requests/:id`, so a device sign-in with no TOTP cannot enroll one and the user does it from a browser. It can step up with TOTP or a real passkey assertion on the relying party's domain. OpenAPI now documents this 403 on those routes and on admin reads. It does not count against `max_concurrent_sessions`. Disabling or deleting the application, or turning the flag off, revokes the sessions it exchanged for. Session responses gain `parentSessionId`, `exchangeClientId` and `exchangeGrantType` on such a session, and application responses gain `sessionExchangeEnabled`. A new migration adds the columns. ADR 0001 D11 is amended to match. + +- 25c4cc6: `third_party_clients_enabled` now gates existing third-party OpenID Provider clients, not only creating and updating them (#555). While it is off, a client that is not first-party is treated like a disabled one: `/oidc/clients/:id` answers `404`, its shared secret does not verify, `/oidc/sign` refuses with `client_inactive`, its notice tokens are refused and back-channel logout skips it. An instance that turned the flag off after registering third-party clients stops serving them on upgrade; turn the flag back on, or mark the applications first-party, to keep them working. + +### Patch Changes + +- 25c4cc6: Two hardening fixes from #556. Every API response now carries `Cache-Control: no-store` unless its route sets its own caching (the JWKS and the admin dashboard's hashed assets do), so tokens, client secrets from create and rotate, recovery codes and delivery payloads are never stored by a cache. Provider requests (OAuth token, userinfo and discovery, and back-channel logout) now go through the address guard in every mode: outside production, `http` and private or loopback addresses are still allowed, but link-local `169.254.0.0/16` and unspecified `0.0.0.0/8` addresses are refused whether named directly or reached through DNS. +- 25c4cc6: The per-user limit on `POST /applications/account-notices` now folds the `userId` to lower case before counting (#553). Upper-case variants of one UUID no longer each get their own allowance of 10 notices an hour. +- 1400b20: A failed OAuth start now says why. The `oauth_login_failed` event with reason `start_failed` carries a `detail` of `redirect_not_allowed` or `provider_unusable`, and the error is logged. A refused redirect is logged by scheme and host only. The answer to the client is unchanged. +- 5b1707a: Starting the server no longer rewrites an env-backed `system_config` row, or warns that an admin change is being reverted, when the stored value only orders its keys differently. Postgres JSONB does not keep key order, so object values such as `lockout_policy` and `flow_rate_limits` looked changed on every boot. + ## 0.22.0 ### Minor Changes diff --git a/adapter-manifest.json b/adapter-manifest.json index 68a07d00..c160883d 100644 --- a/adapter-manifest.json +++ b/adapter-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "apiVersion": "0.22.0", + "apiVersion": "0.23.0", "session": { "subject": "sub", "token": "token", diff --git a/openapi.json b/openapi.json index 49f436fa..921a40db 100644 --- a/openapi.json +++ b/openapi.json @@ -1,6 +1,6 @@ { "openapi": "3.0.3", - "info": { "title": "Seamless Auth API", "version": "0.22.0" }, + "info": { "title": "Seamless Auth API", "version": "0.23.0" }, "components": { "schemas": {}, "parameters": {}, diff --git a/package.json b/package.json index 855c3341..1701673f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "seamless-auth-api", - "version": "0.22.0", + "version": "0.23.0", "description": "Seamless Auth API - A web application server for supporting a Seamless Auth server instance.", "main": "index.js", "type": "module",