Skip to content

v0.23.0

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Nov 14:14
afd4495

Minor Changes

  • ac82c6b: Stream audit events to a SIEM (ADR 0005, #420).

    • Admin API under /admin/audit-sinks: create, update, disable and delete sinks, and send a test record. Every write takes step-up and is audited as admin_audit_sink_created, admin_audit_sink_updated or admin_audit_sink_deleted.
    • OTLP/HTTP with JSON encoding to <endpoint>/v1/logs. Syslog (RFC 5424 over TLS, octet counted) arrives in 0.23.1: creating or updating a syslog sink is refused with a 400 until its structured data ID carries the Fells Code IANA enterprise number (#586).
    • Every record carries the event's seamless-auth-events/1 export line, so the hash chain verifies downstream as an export does.
    • Each sink has its own cursor and retries until its collector takes the events, backing off to one minute. There is no dead letter: a sink reports how far behind the chain head it is.
    • Credentials are never stored. A sink names env vars ending _AUDIT_SINK_SECRET, read at send time.
    • Audit retention no longer deletes events a sink has yet to deliver.
  • 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.

  • 48bafad: Add static custom claims (ADR 0005, #417).

    • Admin API under /admin/custom-claims: list every source with the reserved names, prefixes and budget, and read, replace or delete the set for an instance role, an organization role or an OpenID Provider client. Writes take step-up and are audited by claim name, never value.
    • A set is refused when it uses a reserved name or prefix, a name outside ^[a-z][a-z0-9_]{0,63}$, a value nested deeper than 3, or more than 1 KiB. The role or client must exist.
    • First-party access tokens now carry the claims of the user's instance roles, then of their roles in the active organization (only while the membership exists), then the session's pre_token hook claims, a later source winning per name. Roles of one kind apply in name order. Recovery tokens carry none.
    • OpenID Provider client sets are stored and served here, and applied by the provider in a later release.
    • Permission claims and custom claims now share one 1 KiB token budget (AUTHORIZATION_CLAIMS_BUDGET_BYTES, replacing PERMISSION_CLAIMS_BUDGET_BYTES), so the server adapter's cookie holds the worst case. When both do not fit, permissions give way first and are replaced with permissions_omitted: true, as before. Only when the custom claims do not fit even beside that marker are they all dropped, and the mint writes custom_claims_omitted. Nothing is trimmed. This is a behavior change: a token with custom claims can now carry permissions_omitted at a smaller permission set than before.
    • Retiring an instance role, deleting an organization role, or deleting an OpenID Provider application deletes its custom claims, so adding the name back does not restore them.
    • New audit events admin_custom_claims_updated, admin_custom_claims_deleted and custom_claims_omitted.
  • 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.

  • fe012cb: Add post_login and pre_token login hooks (ADR 0005, #419).

    • Admin API under /admin/login-hooks/:trigger: create or replace, read, delete, rotate the signing secret (24 hour overlap), send a test call, and list invocations (kept 30 days). Every write takes step-up and is audited.
    • Calls are signed like webhooks, with the invocation id as webhook-id, and carry the user, method, organization and request IP and user agent. They never carry a token, code or secret. One attempt, 2 seconds by default and 5 at most.
    • post_login runs once the factor is proven and before the session exists, and may deny. pre_token runs for a new session and on organization switch, and may add claims or deny. Its claims are kept on the session, copied on refresh and to an exchanged session, and dropped when the user leaves the organization they were decided for. Reserved claim names are stripped and audited; the rest are held to 1 KiB.
    • Break-glass administrators and recovery sessions never call a hook.
    • Contract change: sign-in completion routes and POST /organizations/:id/switch can now answer 403 { "error": "login_denied", "message"?: "<the hook's userMessage>" } when a hook denies, and 503 { "error": "login_hook_unavailable" } when a hook set to fail closed times out, answers other than 2xx, or returns a body that does not parse or fit the budget. The hook's reason is audited and never returned.
    • Access tokens can carry pre_token claims beside the existing ones. They can never replace a reserved claim.
    • New audit events login_hook_success, login_hook_denied, login_hook_failed, login_hook_claims_stripped, admin_login_hook_set, admin_login_hook_deleted and admin_login_hook_secret_rotated.
    • CALLOUT_SECRET_ENCRYPTION_KEY is now required in production once a login hook exists, as it is for webhooks.
  • 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 <application> through <instance>. 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.

  • 1bbeeff: Let OpenID Provider tokens carry roles, permissions and custom claims (seamless-auth-oidc-provider#47).

    • GET /oidc/accounts/:id now returns the ADR 0004 permission claims (permissions, org_roles, org_permissions, or permissions_omitted) and customClaims, computed for the session and client the lookup names, with the same shared 1 KiB budget as a first-party token. An omission writes custom_claims_omitted with the client id.
    • Custom claims for a provider token add the client's oidc_client set after the instance and organization role sets and before the session's pre_token hook claims.
    • org_id for a provider token is now the session's active organization, while the user is still a member, instead of the user's oldest membership. This is a behavior change for provider clients that read org_id.
    • /oidc/sign signs these claims only with the values it computes again for that session and client. A different value, a custom claim name it did not compute, or a permission or custom claim set carried in part is refused with the new code claims_mismatch (403), which the provider answers by reading the account again and retrying once. A reserved or unlisted name is still malformed_signing_input. Client credentials tokens still carry none of them.
  • 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 <oidc_issuer>/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.

  • 312e426: Add signed, retried webhooks (ADR 0005, #418).

    • Admin API under /admin/webhooks and /admin/webhook-deliveries: create, update, disable and delete endpoints, rotate the signing secret (the old one signs beside the new one for 24 hours), send a test event, list deliveries with a dead letter filter, and redeliver. Every write takes step-up and is audited.
    • Payloads are signed with the Standard Webhooks scheme (webhook-id, webhook-timestamp, webhook-signature), so any Standard Webhooks verifier checks them.
    • Events are built from committed audit rows, retried eight times over just under 42 hours, then dead-lettered. An endpoint that answers 410, or fails every delivery for three days, is disabled. Deliveries for a disabled endpoint are held, and turning it back on sends what it missed, up to the 30 days deliveries are kept.
    • Every callout to an adopter URL refuses link-local, multicast and the API's own host, never follows redirects, and in production refuses private addresses unless the host is listed in the new CALLOUT_PRIVATE_HOSTS.
    • New env var CALLOUT_SECRET_ENCRYPTION_KEY encrypts stored signing secrets. Production requires it once a webhook exists.
    • New audit event session_created, written once per completed sign-in. Deleting a passkey is now audited as credentials_deleted, and passkey registration names the credential.
  • 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

  • e7d4e1e: The Docker image now bundles admin console v0.14.0 at /console, up from v0.12.0. It adds the Roles page for permissions and organization roles (v0.13.0), and the Webhooks, Login hooks, Custom claims and Audit streaming pages under Integrations (v0.14.0).
  • 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.