Repository navigation
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 asadmin_audit_sink_created,admin_audit_sink_updatedoradmin_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/1export 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.
- Admin API under
-
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 thesidit was given, with the same sender and the sameoidc_backchannel_logout_sentandoidc_backchannel_logout_failedevents 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 abackchannel_logout_urishould 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_tokenhook 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, replacingPERMISSION_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 withpermissions_omitted: true, as before. Only when the custom claims do not fit even beside that marker are they all dropped, and the mint writescustom_claims_omitted. Nothing is trimmed. This is a behavior change: a token with custom claims can now carrypermissions_omittedat 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_deletedandcustom_claims_omitted.
- Admin API under
-
bcab4d1:
GET /oidc/payloads/DeviceCode/:idnow returns a device code for 10 minutes after it expires, with itsexp, 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 codeexpired_token(RFC 8628 section 3.5) rather thaninvalid_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-tokento carry subjectseamless-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 setsubtoseamless-auth-external-delivery. -
fe012cb: Add
post_loginandpre_tokenlogin 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_loginruns once the factor is proven and before the session exists, and may deny.pre_tokenruns 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/switchcan 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'sreasonis audited and never returned. - Access tokens can carry
pre_tokenclaims 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_deletedandadmin_login_hook_secret_rotated. CALLOUT_SECRET_ENCRYPTION_KEYis now required in production once a login hook exists, as it is for webhooks.
- Admin API under
-
25c4cc6: Application notice text can no longer carry its own links (#554).
POST /applications/account-noticesrefuses with400atextthat contains a URL, awww.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. TheSent 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 intext. -
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 withGET /admin/applications/:id/linksand removes one withDELETE, each write with step-up and audited asadmin_oidc_application_link_addedoradmin_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-noticestakes an optionalclientId: 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 same404 notice_unknown_useras an unknown user. Applications gainnoticeActionOrigins, barehttpsorigins that a notice's action link may point at besides thehttpsredirect origins, and every application response listsnoticeActionOrigins,linksandlinkedFrom. A new migration adds theoidc_client_linkstable 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_rolesandorg_permissionsbesideorg_id, andGET /users/meandGET /users/me/permissionsreport the same. Organization roles never appear inroles, so a guard onadmindoes not pass for an organization admin. Organization roles are now a catalogue, managed withPOST /admin/organization-rolesandPATCHandDELETE /admin/organization-roles/:roleName, listed byGET /admin/roles, and mapped to permissions withPUT /admin/roles/organization/:roleName/permissions. A new migration seeds it with the built-inowner,adminandmemberand every value memberships already hold, and rewrites no membership. Behavior changes: assigning a role outside the catalogue now answers 400.PATCH /organizations/:id/members/:userIdnow needs step-up. Only an owner or an instanceadmin:writecan grant or removeowner, 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 asorganization_member_added,organization_member_updatedandorganization_member_removed, and a refused change asorganization_member_role_grant_refused. Fixes: removing a member now reaches the token. Previously every refresh copiedorg_idfrom 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 aswebauthn_login_failedorstep_up_failedwith reasonBackup eligibility differs from registration). A passkey counts as device-bound, and earnsacr=phrhandamr=hwk, only when it was registered device-bound with attestation verified against the FIDO Metadata Service. Deployments that use the defaultnoneattestation, and credentials registered before attestation was recorded, now getphrandswkinstead ofphrhandhwk. -
da66738: Roles can now grant permissions, and access tokens carry them (#415, ADR 0004). An admin manages the catalogue with
GETandPOST /admin/permissionsandPATCHandDELETE /admin/permissions/:permissionName, reads roles withGET /admin/roles, and replaces what a role grants withPUT /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 inavailable_roles, and removing one also removes its mappings, so adding the name back later does not restore old grants. Access tokens gain apermissionsclaim, 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 carriespermissions_omitted: trueand a guard readsGET /users/me/permissions, which answers from the database on every call.GET /users/megainsuser.permissions. Changes reach a token when it is next issued, within the access token lifetime. New migration adds thepermissionsandrole_permissionstables. -
1bbeeff: Let OpenID Provider tokens carry roles, permissions and custom claims (seamless-auth-oidc-provider#47).
GET /oidc/accounts/:idnow returns the ADR 0004 permission claims (permissions,org_roles,org_permissions, orpermissions_omitted) andcustomClaims, computed for the session and client the lookup names, with the same shared 1 KiB budget as a first-party token. An omission writescustom_claims_omittedwith the client id.- Custom claims for a provider token add the client's
oidc_clientset after the instance and organization role sets and before the session'spre_tokenhook claims. org_idfor 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 readorg_id./oidc/signsigns 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 codeclaims_mismatch(403), which the provider answers by reading the account again and retrying once. A reserved or unlisted name is stillmalformed_signing_input. Client credentials tokens still carry none of them.
-
2f1c3db:
GET /system-config/publiccan 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 fromOIDC_CLI_CLIENT_ID, or set throughPATCH /system-config/admin), names the application. The response gains an optionaloidcProvider: { 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 fixesPATCH /system-config/adminanswering 500 when it set a nullable key such asoidc_issuer,hosted_uiormax_concurrent_sessionstonull: the row is now removed, which reads back asnull. No migration is needed. Needs the@seamless-auth/typesrelease 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/verifywhen the adapter lists onlyaccount_notice_email) goes out through the API's own email or SMS transport when one is configured, and is recorded assentorfailedrather thannot_sent_external_delivery. The same applies to the recovery completed and recovery code used notices and toPOST /applications/account-notices. A recovery that previously answered503 recovery_notice_failedfor 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
sessionExchangeEnabledon for a first-party public native application, with step-up, audited asadmin_oidc_application_session_exchange_updated. Such a client can ask for a 60 second access token for<oidc_issuer>/seamlesswith the single scopesession:exchange, and/oidc/signrecords each one.POST /sessions/exchange(adapter: false, RFC 8693 parameter names) consumes that token once and returns the/refreshbody plusissued_token_type, for a new session linked to the provider sign-in. A second presentation answers400 invalid_requestwithtoken_already_usedand revokes the session the first one created, audited assession_exchange_replayed. A successful exchange is audited assession_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_requireduntil it has a fresh step-up, on every route behindrequireAdminand every/organizationsroute exceptPOST /organizations/:id/switch, reads included, and on/totp/enroll/*,/totp/disable,/webauthn/register/*,POST /recovery-codes,DELETE /users/credentials,/registration/phone,/registration/phone/verifyandDELETE /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 againstmax_concurrent_sessions. Disabling or deleting the application, or turning the flag off, revokes the sessions it exchanged for. Session responses gainparentSessionId,exchangeClientIdandexchangeGrantTypeon such a session, and application responses gainsessionExchangeEnabled. 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/webhooksand/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_KEYencrypts 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 ascredentials_deleted, and passkey registration names the credential.
- Admin API under
-
25c4cc6:
third_party_clients_enablednow 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/:idanswers404, its shared secret does not verify,/oidc/signrefuses withclient_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-storeunless 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,httpand private or loopback addresses are still allowed, but link-local169.254.0.0/16and unspecified0.0.0.0/8addresses are refused whether named directly or reached through DNS. - 25c4cc6: The per-user limit on
POST /applications/account-noticesnow folds theuserIdto 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_failedevent with reasonstart_failedcarries adetailofredirect_not_allowedorprovider_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_configrow, 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 aslockout_policyandflow_rate_limitslooked changed on every boot.