Changelog#
All notable changes to this project are documented here. The format follows Keep a Changelog 1.1.0, and the project will use Semantic Versioning once a first version is tagged. During 0.x, any minor version can contain breaking changes; they are listed under Changed or Removed with migration notes.
Security fixes name the affected versions and link the advisory (QA-008, GOV-003). Requirement IDs refer to docs/plan.md.
[0.2.0-alpha.1] - 2026-10-13#
Added#
- First public pre-release under
proudbase/proudauth: versioned Linux/macOS binaries, a multi-architecture Linux amd64/arm64 container image, source and per-platform image SBOMs, release checksums, keyless image signatures and GitHub build-provenance attestations (DEP-003, SEC-016, GOV-005). - Source-free Docker Compose installer with complete adjacent
.envdocumentation, production HTTPS/SMTP and loopback-only demo profiles, stable file-backed keys, PostgreSQL 18.6, separated migration/runtime roles and one-time administrator bootstrap (DEP-002, DEP-004, DEP-006, OPS-013). - Independent public Go, Node/TypeScript, Dart, PHP and .NET SDK repositories,
with standalone CI and a pinned Go SDK dependency. Project information and
documentation are maintained separately in
proudauth-website. - Measured laptop capacity/adversarial/soak and restore evidence carried into the public service repository with explicit build provenance and sizing caveats (NFR-003..NFR-008, DEP-019, OPS-007).
Changed#
- Go module path is now
github.com/proudbase/proudauth; applications import Go SDK packages fromgithub.com/proudbase/proudauth-sdk-go. SDK source no longer ships inside the service tree; package/registry publication remains distinct from repository publication. - Release publication reuses successful main CI only for the exact tagged commit, otherwise running the full reusable CI before publishing.
- GitHub Container Registry is the automated organization-owned registry; the Docker Hub mirror is published separately by the maintainer.
Fixed#
- Deployment initialization rejects accidental public demo bindings, mismatched demo issuer ports, globally trusted proxy ranges and invalid pool capacity. Repeated initialization preserves credentials and mounted secret inodes.
- Offline and Compose release archives exclude generated environment files, secret directories, installed configuration and initializer locks.
This is a pre-release, with no completed external security review or conformance certification. Existing benchmark numbers describe their recorded development builds and are not guarantees for this release.
Unreleased#
No changes recorded after 0.2.0-alpha.1.
Historical development before 0.2.0-alpha.1#
The entries below preserve evidence from development before repository separation and the first public release. References to the private monorepo, unpublished artifacts, local tunnels and embedded SDK paths describe that historical setup; they are not current deployment instructions or release claims. Changed, Fixed and Security fixes describe earlier development behavior. No external security review (SEC-018) or certification (QA-006) was completed for that work.
Added#
- Public documentation website (
docs-site): complete source-derived guides, all OpenAPI operations, five SDK guides, full-text search, downloadable contracts and a read-only API explorer.make docs-sitebuilds/checks offline with loaded images;make docs-serveretains the documentation, service overview and a sanitized live status page behind the existing Cloudflare tunnel. Deployment credentials and private source/seed files are excluded from publication. - Cloudflare Tunnel test profile (
docker-deploy/config.tunnel.yaml; DEP-002, DEP-004):auth.proudauth.comserves the public identity API through the existing host connector, with a canonical HTTPS issuer, strict hostname checking, Secure cookies and an explicit local proxy. Operator endpoints remain on loopback; development email remains private. The deployment README records activation and restart settings. - Standalone local image deployment (
docker-deploy; DEP-002, DEP-004): a PostgreSQL 18, server, worker and Swagger Compose stack using already loaded ARM64 images. Initialization generates a private.env, preserves existing credentials and provisions realmmainonce. Local validation covers the stack and .NET SDK interoperability with the running server. - .NET SDK (
sdk/dotnet, unpublished 0.2.0, .NET 8+; SDK-001..SDK-011):ProudAuth.Sdkimplements offline verification (RS256, PS256, ES256, Ed25519), multi-issuer routing, introspection and audited support access, bounded revocation state, OIDC code + PKCE with ID-token validation, single-flight refresh and logout, and an administration client with cursor pagination, problem details and redacted one-time secrets.ProudAuth.Sdk.AspNetCoreadds the authentication scheme and middleware. Shared token/support fixtures and Go signature vectors run in its test executable; CI is configured for .NET 8 on Linux, macOS and Windows. BouncyCastle.Cryptography 2.7.0 supplies Ed25519 and RSA-PSS verification. Fifteen manual live-server checks pass alongside the 342 SDK tests; the live checks are separate from CI. No NuGet publication is claimed. - Private source handoff (GOV-005, SEC-016): the owner authorized committing/pushing the implementation and evidence while keeping the repository private. Public releases, tags and hosted signing/attestation are explicitly deferred. Host-built executables stay outside Git and the Docker build context; traceability evidence uses versioned source rather than generated TypeScript declarations. Frozen benchmark/tool outputs retain their original whitespace and checksums.
- Local production-image and release preparation (SEC-016, DEP-003, DEP-004, DEP-017, GOV-005): Image/SBOM/package evidence records passing TLS/read-only/non-root image checks, zero image OS-package vulnerability matches, all 12 stripped target builds, image/host binary equality and 582 verified offline-bundle files. Source/image SPDX SBOMs and exact hashes are retained; temporary resources are removed. Module-only OpenPGP findings are explicitly triaged. No official tag, upload, signature or trusted provenance attestation is claimed.
- Current dependency and artifact checks (QA-002, SEC-016, DEP-003): Published results record zero reachable findings in Go source and all 12 release binary targets, zero npm advisories and zero final Staticcheck findings. The frozen official database and an unimported OpenPGP module-level finding have explicit provenance/triage. Eight workflow Actions and the Dockerfile frontend now use immutable references, guarded by contract checks.
- Local SDK runtime evidence (SDK-009, SDK-010, QA-002, QA-004): Offline check results publish passing Node.js 25.9.0 (116 tests), Dart 3.13.3 (216 tests) and PHP 8.5.8 (157 tests, 3078 assertions) profiles with source hashes, type/analyzer/format/lint checks and unchanged locks. Current Go 1.27.2 SDK results come from the separate complete module check. Declared minimums, configured CI versions and measured local runtimes remain distinct; dependency vulnerability review and external assurance are open.
- Offline two-OS binary reproduction (M0, DEP-003, SEC-016):
make repro-localfreezes the Go build inputs and compares all 12 release targets between a host compiler and the pinned cached Linux image, using Go 1.27.2, separate caches, no downloads and no Linux network. A temporary host SDK is bootstrapped from cached source when needed. All 12 real binary pairs match byte-for-byte across two invocations; source/target hashes and scope limits are published. Controlled regressions reject one changed output, incorrect SDK/version and unexpected caller flags while checking cleanup. This is one physical host; official release, image/archive and independent-machine reproduction remain unverified. - Controlled HTTPS metadata benchmark (MCH-011, NFR-006, NFR-008):
metadata-fetchalternates a valid workload grant with unknown-key, malformed/oversized JWKS, HTTP failure, redirect, timeout and live denied IPv6 profiles. The counts-only TLS source proves actual upstream contacts and unchanged cooldowns; assertions and private keys stay private. Included inbench-localand all 26 benchmark smoke scenarios, with strict refusal, TLS, response-size, cancellation and result-privacy regressions. Published reference results pass the full-seed cohosted 60-second/concurrency-32 profile with actual upstream spacing/counts, build/configuration hashes and latency caveats. - Metadata fetch regressions (FED-017, MCH-011, ADV-004, ADV-007, NFR-006): concurrency-32 TLS bursts prove shared cold/expired/unknown-key fetches, negative-response cooldowns and recovery. Additional cases cover issuer mismatch, redirect, response size, timeout, DNS deadline, caller cancellation, cache invalidation and last-good workload fallback.
- Compound audit filter benchmark (NFR-004, NFR-006, NFR-008):
complex-audit-filterowns public application reports in two seeded organizations and verifies eleven simultaneous search parameters, exact sequence pages and provenance, literal prefixes, typed network/time refusals and changed-filter cursor rejection. Included inbench-localand the complete benchmark smoke test; setup and reporter-token renewal are unmeasured. Published reference results pass the cohosted 60-second/concurrency-32 profile on 10000 reports, with environment, hashes, redacted configuration and aggregate-latency caveats. - SCIM Group filter benchmark (NFR-003, NFR-004, NFR-006, NFR-008):
complex-group-filtercreates owned Groups matching the seed organization count and owned member Users matching its large-group size. One Group has every member; others have one. It verifies 64-node member/name predicates, 65-node/1025-byte refusals, exact bounded pages, schemas, references and ETags, including member projection, under unchanged connector budgets. It is included inbench-localand the complete benchmark smoke test. Published results record the cohosted 60-second/concurrency-32 baseline and its budget caveats. - SCIM scan-boundary regressions (PROV-002, PROV-004, NFR-004, NFR-006): Users and Groups at 20,000 resources retain exact totals and bounded pages; 20,001 broad/compound scans refuse without partial data, while indexed equality returns its exact owned resource at concurrency 32.
- Shared OTP guessing regressions (TEST-021, NFR-006, NFR-007, MFA-013): email, SMS, HOTP, mixed-method and phantom-account bursts use eight source IPs and three independent runtimes/pools. The unchanged ten-proof account budget, exact challenge/transaction failures and audits, exhausted-budget refusals and next-window authority are checked. Evidence and structured results record the cross-method failure, correction and passing full offline check.
- Directory transport consistency regressions (PROV-012, PROV-014, NFR-007): actual LDAPS sockets are dropped after a committed user page across three independent pools. Full and promoted incremental failover enforce complete-source deletion, while exhausted transport preserves memberships/cursor until a full retry succeeds. Pre/post evidence and frozen-build results include three race-detector repetitions with existing crash/sync support and the passing full offline race-detector check.
- Native directory worker crash regressions (PROV-007, PROV-012, NFR-007, TEST-058): a separately exec'd worker task is killed after one committed/checkpointed LDAPS page. Three replicas refuse early takeover and recover full/incremental scans at lease expiry with original account IDs, exact Group authority, cursor and audit counts. Six OS kills pass across three race-detector repetitions; evidence and scope and structured results distinguish worker-task death from production-entrypoint and host/DB/network failures.
- Directory scheduler and lease-recovery regressions (PROV-012, NFR-007, TEST-058): concurrency-32 schedule claims across independent pools verify rollback while blocked, exact full/incremental slots and one leased handler per run. A partial scan is reclaimed at lease expiry; the returning worker cannot modify the recovered state. Six generic job outcomes cover stale finalization/progress, attempt fencing and protected results. Evidence and the full offline verification result preserve source hashes, environment and test/package outcomes.
- Directory replica synchronization stress (PROV-007, PROV-012, NFR-007): concurrency-32 public requests and three competing workers execute paged full, incremental dry-run, applied incremental and repeat full sync against a loopback LDAPS test directory. All independent pools check sole run/job leases, exact account/link/membership/audit effects, durable cursors, Group-derived authority and preserved administrator state. Three race-detector repetitions pass; evidence and limits distinguish this profile from real AD interoperability and fault testing.
- Mixed group deletion regressions (NFR-007, QA-008): a controlled lock-order check, connector external-ID reuse, legacy LDAP Group repair and actual outbound SCIM cleanup complement concurrency-32 administrator/connector deletion races across three independent pools. Before/after evidence records three race-detector repetitions and exact durable effects.
- SCIM Group replica stress (PROV-007, PROV-008, NFR-007): concurrency-32 conditional connector/admin renames require exactly one winner across three independent runtimes/pools. Membership replacement versus administrator addition covers concurrent writes and both causal orders; controlled deletion removes both role sources once while keeping organization memberships active. Exact versions, member revisions, audit counts and effective permissions pass three race-detector repetitions. Evidence and scope.
- Shared-SSO replica revocation stress (NFR-007, SES-014, SES-015, SES-021): three grants from two clients share one real browser SSO session across three independent runtimes/pools. Concurrency-32 races cover client-local revocation, offline/bound refresh reuse, one-session and account-wide revocation; post-commit checks preserve each logout scope, independent same-user sessions and unrelated users. Exact lineage, reuse/session audit/outbox counts and silent SSO checks pass three times with the race detector. Evidence and limits.
- Native-process refresh benchmark results (NFR-003, NFR-007, NFR-008):
make bench-replicasruns three independent server processes behind the loopback, per-requestidentity-bench replica-proxy, with no cross-backend failover or one-use POST replay. The local runner records per-node CPU, counts-only routing and independent SQL lineage/reuse evidence. Both full-cardinality 60-second refresh profiles pass; redacted configuration, raw results and their single-laptop limits are published. - Replica refresh and revocation stress (NFR-007): concurrency-32 bursts across three independent pools check foreign-client refusals, exact successor/reuse counts for session JWT, opaque and offline JWT grants, and post-commit rejection after concurrent session/account-wide revocations. Grant-level regressions cover UserInfo, account APIs and exchanged descendants while unrelated grants remain active. Evidence and topology limits.
- Replica ownership, role and connector stress (NFR-007): concurrency-32 bursts across three independent pools prove one self-releasing ownership transfer, one role grant/edit/revoke transition and one conditional SCIM/admin writer. Each pool checks exact versions, permissions and audit/outbox counts. Additional regressions prove idempotent template setup and competing expiry-worker cleanup. Results and topology limits record three race-detector repetitions without treating them as capacity.
- Independent-replica recovery and invitation stress (NFR-007): three HTTP replicas have separate pools, transaction limits, keyrings and caches. Barrier-started concurrency-32 bursts require exact refusals and committed one-use/membership/role/audit/outbox counts from every pool. Three race-detector repetitions pass without raising account budgets. Recorded evidence and remaining scope distinguish these local consistency tests from multi-machine capacity.
- Complex SCIM filter benchmark (NFR-006, NFR-008):
complex-filtercreates an isolated connector with the seed's user cardinality through public APIs, preserving its 1200/minute budget. It measures 64-node scans, 65-node/1025-byte rejection, exact active/inactive totals, clamped pages and owned IDs, with strict SCIM errors/retry guidance. Fixture creation is unmeasured and paced; measured operations are unpaced. Unit and smoke checks cover the profile. Passing full-cardinality laptop results, with the initial failure preserved and budget-refusal limits explained, are published. - TOTP guessing benchmark (NFR-006, NFR-008):
otp-guessenrolls an isolated owned factor and attacks eight MFA transactions without raising proof or identifier budgets. It excludes legitimate codes, requires retry guidance and proves transaction closure before retirement; private factor/continuation authority is cleared on close. The local runner checks ten evaluated proofs against stored failure/audit counts. Concurrent and sequential database-backed regressions cover the shared account budget and five-failure transaction retirement. Passing laptop results and the limits of this TOTP-only profile are published. - Webhook worker benchmarks (NFR-003, NFR-005, NFR-008): isolated
membership add/remove cycles prove signed, request-correlated callbacks
and exact delivered records.
webhook-retryinjects one503per event and checks identical ID/body, fresh timestamp and two attempts under the default backoff;mixed-deliverycovers all 12 workload components. Portable receiver snapshots record retry/recovery times without secrets or message contents. The local runner selects the soak scenario and permits only the loopback/32egress needed by its own receiver. Passing local delivery/retry, twelve-component mix and every window of a ten-minute delivery soak with actual retries and key activation, along with the initial seed-backlog failures and their limits, are published. - SMTP worker benchmarks (NFR-003, NFR-008):
email-deliverymeasures enqueue through confirmed worker/provider acceptance;mixed-emailcombines it with all nine extended workloads. A loopback-only SMTP sink exercises the actual transport and retains counters, never message bodies. The local runner switches after bootstrap, records the SMTP configuration, checks receiver counts and cleans up its own listener. Latency includes job polling; acceptance does not prove inbox delivery. Passing laptop results for the independent workload, the ten-component mix and a flood limited to three actual SMTP messages are published with their limits. - Extended workload benchmarks (NFR-003, NFR-008):
passkey-assertionexercises discoverable ES256 software credentials through complete sign-in and PKCE exchange;scimchecks a completed five-request user lifecycle under unchanged connector budgets.mixed-extendedcombines them and imports with the original workloads. Private fixtures are prepared through public APIs before timing; the originalmixedprofile is preserved.make bench-localenables passkeys only in its dedicated realm and records explicit localhost RP/origin settings. Measured results for all three new scenarios, including the initial configuration failure, are published with software-authenticator and SCIM pacing limits in benchmarks.md. - Go resource-server sender verification (TOK-019, MCH-014, TEST-067):
verifier.SenderVerifierverifies request-bound DPoP proofs or TLS client certificates with offline JWT verification or strict-online introspection. It checks the method, configured public URL, access-token hash, key or certificate binding and atomic single-use proof state. Concurrent replay, state-store outages, ambiguous headers/JSON and forged proxy headers deny access. A separate TLS API exercises actual ProudAuth JWT/opaque DPoP tokens and certificate-bound JWTs inTestResourceSenderVerifier. The exact profile, shared-store requirement and TLS proxy boundary are in resource-servers.md. - Published laptop benchmark results and an offline local runner
(NFR-003..NFR-008, M4):
make bench-localstarts dedicated PostgreSQL 18, builds host server/worker/tool binaries from cached modules, bootstraps an MFA administrator, seeds themake benchcardinalities, and checks every workload, adversarial and refresh-race scenario plus a 10-minute mixed soak crossing token expiry, signing-key activation, retention, vacuum and backup. Results, redacted configuration, exact environment and commands, pass criteria and caveats are in benchmarks.md anddocs/operations/bench-results/2026-10-11-laptop/. The runner refuses existing resources and cleans up only its own processes and data; the seed file and administrator credentials are never published. - Interoperability with Keycloak 26.8.0 (QA-004, M4;
SAML-010):
scripts/interop-keycloak.sh(make interop-keycloak, jobkeycloakof the weeklyinterop.ymlworkflow) starts Keycloak (start-dev, HTTPS), PostgreSQL 18 and ProudAuth built from the checkout in throwaway containers and plays the browser and the login UI: Keycloak as OpenID Connect relying party of ProudAuth (code + PKCE S256,client_secret_basic, ID token through the JWKS, UserInfo, RP-initiated and back-channel logout), Keycloak as upstream OpenID Provider of a ProudAuth OIDC connection (just-in-time account and link, upstream logout), Keycloak as SAML service provider of a ProudAuth SAML application (signed requests and responses, persistent NameID, encrypted assertions, single logout) and as SAML identity provider of a ProudAuth SAML connection, each side configured from the other's discovery document or metadata; replayed codes, callbacks,AuthnRequests and SAML responses, tampered SAML messages and wrong client secrets are refused. Profiles, the Keycloak settings that matter and what is not covered: interoperability.md. - Interoperability with ZITADEL v4.16.3 as a SCIM service provider
(QA-004, PROV-010, PROV-017):
scripts/interop-zitadel.sh(make interop-zitadel, jobzitadelof the weeklyinterop.ymlworkflow) starts ZITADEL and PostgreSQL 18 in throwaway containers and pushes ProudAuth accounts to ZITADEL's/scim/v2/{orgId}/Userswith a least-privilege service account's token: creates, profile and email changes, suspension, deactivation and deletion by an organization and a realm connector, a token revoked in ZITADEL and rotated, and reconciliation (dry run, drift, a deleted and an unmanaged user), all read back from ZITADEL. The tested profile, what ZITADEL requires (one media type inAccept;nameandemails; no groups) and what is not covered are in outbound-provisioning.md and interoperability.md. - Independent OpenID Connect relying-party test (QA-004, OIDC-001,
OIDC-003, TOK-011):
TestOIDCRelyingPartyLibrarysigns in throughgithub.com/coreos/go-oidc/v3v3.18.0 andgolang.org/x/oauth2v0.37.0 (new test-only dependencies): discovery, code flow with S256 PKCE, the library's ID-token verification through the JWKS (including a planned rotation and a withdrawn key), UserInfo and refresh, and refusal of another client's or a tampered ID token. - Provisioning and federation attack suites (M4 exit gate):
attack-suites.md maps each attack on
provisioning and federation to the tests that mount it;
make attack-suitesruns them and CI runs them as their own step (TestAttackSuitesExistkeeps the document and the Makefile in step).
Protocols#
- OAuth 2.0 authorization code flow with mandatory PKCE
S256, exact redirect matching and single-use, client-bound codes (TOK-001, TOK-002, OIDC-001, TEST-028, TEST-029). - Refresh-token families with rotation, reuse detection and family/session revocation (SES-006, SES-009, DB-006, TEST-030..TEST-032).
- Client credentials; device authorization grant (RFC 8628); token exchange
(RFC 8693, TOK-016, TOK-017) for access-token subjects with explicit audience
transitions and a bounded
actchain; pushed authorization requests (RFC 9126); DPoP proofs andcnf.jkt-bound access tokens at the token endpoint (RFC 9449). - Introspection (RFC 7662) limited to clients registered as introspectors of the token's audience (TEST-037); revocation (RFC 7009).
- OpenID Connect: discovery and RFC 8414 metadata generated from implemented
features, JWKS, ID tokens, UserInfo, RP-initiated logout, back-channel logout,
RFC 9207
issparameter. - JWT access tokens (
at+jwt, one audience per token,token_kind) and opaque access tokens per resource; opaque support tokens (pa_it_). - OIDC
claimsparameter (OIDC-008): validated, stored with the grant and used only to narrow release per target. Anid_tokenmember limits the ID token to the listed user claims (plusemail_verified/phone_number_verifiedwhenemail/phone_numberis listed) and auserinfomember limits UserInfo the same way, always within the granted scopes; a missing member means scope-based release for that target.acrrequests raise the assurance requirement. Discovery now advertisesclaims_parameter_supported: true(migration0017,TestClaimsRequestAndStaticClaims). prompt=select_accountstops for the user to keep or switch the signed-in account (POST .../auth/transactions/{id}/account, OIDC-005).- Per-resource
signing_algorithmfor access tokens, limited tosecurity.allowed_algorithms; key rotation refuses to strand pinned resources (TOK-005,TestResourceAlgorithmProfile). - Organization scope ceilings (
allowed_scopesin organization policy) and scoperequired_permissions(migration0015), applied at every issuance including refresh (TOK-008,TestScopeSubjectAndOrganizationPolicy). - Client
token_policy.max_session_age_seconds: older SSO sessions are not reused for that client (SES-002). - Durable pairwise subject identifiers: derived once and stored in
pairwise_subjects(migration0018), so they no longer change when the active root-key version changes (OIDC-010, APP-012,TestPairwiseSubjects). - Reviewed pairwise sectors (OIDC-010): a pairwise client's
sector_identifierdefaults to the host its redirect URIs share and is required (422) when they span several hosts; every redirect URI must be the sector host or one of its subdomains, so a client cannot claim another application's sector. Clients registered in the same sector receive the same pairwisesub(TestPairwiseSectors). - Scope step-up (AUT-010, migration
0028): scopes acceptstep_up_acr(urn:proudauth:acr:mfaorurn:proudauth:acr:phishing_resistant) andmax_auth_age_seconds(60–86400); a scope withsensitivity: privilegedand nostep_up_acrrequires MFA. Requesting such scopes adds the strongest requiredacrto the transaction'sacr_valuesand lowersmax_ageto the shortest age, so an SSO session is stepped up (never downgraded) and a remembered device cannot skip the MFA prompt. At code, device-code and refresh issuance a scope is left out of the access token when the grant'sacrranks below the scope's or itsauth_timeis older than the scope allows; the client regains it with a new authorization request (TestScopeStepUp; oauth-oidc-profile.md). - Realm access-token lifetime limits (TOK-018): realm policy
tokens.access_token_ttl_seconds(every access token),tokens.service_access_token_ttl_seconds(service principals) andtokens.single_factor_access_token_ttl_seconds(user tokens whoseacris belowurn:proudauth:acr:mfa), each 30–86400 seconds or 0 for no limit, can only shorten the lifetime set by the resource and the client, andtokens.max_access_token_ttlstill applies (TestTokenLifetimePolicy; tokens.md).tokens.impersonation_access_token_ttl_seconds(and the realm-widetokens.access_token_ttl_seconds) also bound support grants, whose tokens live as long as the grant. - Environment separation (APP-015): a client's
environment(development,stagingorproduction; defaultproduction) is fixed at registration, so a development client and its credentials are never promoted in place:PUT /clients/{client}with another value is refused (422onenvironment). A client registered in a project defaults to the project's environment and must match it (422). Token exchange refuses (invalid_grant) a subject token issued to a client of another environment than the presenting client's, so production clients never accept development tokens (TestEnvironmentSeparation; oauth-oidc-profile.md). - Claim mappings (APP-011): besides static values, a client's
id_token_claimsandaccess_token_claimsaccept attribute mappings{"$attr": "...", "transform": "lower" | "upper", "default": "..."}.$attrnames one approved attribute:user.given_name,user.family_name,user.display_name,user.locale,user.zoneinfo, orprofile.<name>for a realm profile attribute markedclaimable: trueinprofile.attributes; there are no expressions. Each map holds at most 20 claims with names of at most 64 characters; string values are cut to 256 characters before the transform, and an absent (or not claimable) attribute yieldsdefaultor no claim. Attribute values are released only when the grant includes theprofilescope (see Security fixes), and a profile attribute cannot be bothclaimableanduser_editable. Mappings never set reserved claims, and in ID tokens never set OIDC standard user claims or claims already released by scope. Service-principal tokens have no attributes, so only a mapping'sdefaultcan apply to them (TestClaimMappingsintests/e2e/tokens_test.goandinternal/application/claimmap_test.go; oauth-oidc-profile.md; the configuration document schema describes them asClaimAttributeMapping). - SAML 2.0 identity provider and broker (SAML-001..SAML-010, APP-018,
FED-014, TEST-051; migration
0060;internal/saml,internal/saml/idp,internal/federation/saml.go;TestSAMLIdentityProvider,TestSAMLKeyRollover,TestSAMLBroker), off unlesssaml.enabledis set (newsaml.*keys). SAML applications (/api/v1/realms/{realm}/saml/applications,realm.clients.read/.write) are service providers backed by a client record withprotocol: "saml"that is not an OAuth client: SP-initiated SSO over HTTP-Redirect and HTTP-POST (/realms/{realm}/saml/sso), responses over HTTP-POST only to exactly registered ACS URLs, a browser-bound continuation (pa_samlcookie), signed error responses, persistent (per-applicationsps_...),emailAddress(verified only),unspecifiedand transient NameIDs (full URNs), attribute mappings with protected names,RequestedAuthnContextmapped to step-up, organization binding and consent, optional encrypted assertions (AES-GCM with RSA-OAEP only), IdP-initiated SSO off by default and per application, and SP-initiated single logout from signed requests only that reportsPartialLogout. SAML connections (/api/v1/realms/{realm}/saml/connections,realm.identity_providers.read/.write) are federation connections of kindsamlwith uploaded IdP metadata, signed HTTP-RedirectAuthnRequests, single-use correlation (InResponseToplus a 256-bitRelayState), assertion-ID replay records, pinned certificates with overlap for rollover, strict audience, destination, recipient and time checks, and email trusted only withtrust_email_verified; unsolicited and encrypted responses are refused. Both roles use a per-realm RSA 3072 SAML key set with pre-published rollover (/api/v1/realms/{realm}/saml/signing-keys,realm.keys.read/realm.keys.rotate; forced activation is audited asemergency), a parser that refuses DTDs, comments, duplicate IDs and oversized or deep documents before building a tree, andgoxmldsigv1.6.1 with SHA-1 refused and no algorithm exception. Auditsaml.*. Tested only against the repository's own test SP and IdP; no interoperability test against a third-party SAML product has been run (saml.md, SAML threat model, ADR 0018). - OpenID Connect Session Management 1.0 (OIDC-014), opt-in per realm
(
sessions.check_session_iframe, default off): discovery listscheck_session_iframe(I/oidc/check-session), authorization responses with a code carrysession_state(salted, bound to the client and the redirect URI's origin; inside the JWT with JARM), and the script-readablepa_opbscookie holds a keyed digest of the session ID, never the session secret. The iframe answers only origins registered for the client. Third-party cookie blocking makes every check reportchanged, and server-side revocations are not visible to it; use back-channel logout for those (TestCheckSessionIframe; oauth-oidc-profile.md). - Front-channel logout (SES-018, migration
0050): clients registerfrontchannel_logout_uri(same scheme, host and port as a registered redirect URI) and optionallyfrontchannel_logout_session_required(addsissandsid). When the logout endpoint ends a session, browsers get a200page that loads the URIs of the active participating clients in hidden frames (its CSP frames only those origins and allows no script) and continues to the post-logout redirect after two seconds; callers sendingAccept: application/jsongetfrontchannel_logout_urisandredirect_toto render the frames themselves. Delivery is never confirmed; back-channel logout remains the reliable mechanism. Discovery setsfrontchannel_logout_supportedandfrontchannel_logout_session_supported(TestFrontChannelLogout; oauth-oidc-profile.md). - Encrypted ID tokens and signed or encrypted UserInfo (TOK-020, OIDC-013):
per-client
id_token_encrypted_response_alg/enc,userinfo_signed_response_alganduserinfo_encrypted_response_alg/enc(stored in the newclients.protocol_options). Key managementRSA-OAEP-256,ECDH-ES,ECDH-ES+A128KW,ECDH-ES+A256KW; content encryptionA128GCMorA256GCM, always explicit (RSA1_5, SHA-1RSA-OAEPand CBC-HMAC are refused,422). ID tokens become nested JWTs encrypted to the client's newest fitting registered key; UserInfo answersapplication/jwt. A client without a fitting key gets400 unauthorized_clientat the token endpoint and400 invalid_requestat UserInfo (TestEncryptedResponses; response-security.md). - JARM (ADV-002):
response_mode=query.jwt(orjwt) at the authorization endpoint, in PAR and in the headless start returns one signedresponseparameter (iss,aud, 10-minuteexp, andcodeandstateor the error), optionally encrypted (authorization_encrypted_response_alg/enc); errors returned to the redirect URI use the same mode.fragment.jwt,form_postandform_post.jwtare not offered. Discovery lists the response modes and algorithms (TestJARM; response-security.md). - Dynamic client registration (APP-016, APP-017, ADV-003, RFC 7591/7592,
migration
0052): off by default (realm policyclient_registration);POST I/oauth/registerwith an administrator-issued initial access token (pa_iat_, bound to non-privileged scopes, limited uses, default expiry 7 days) or, withopen, without one (then always held for approval); registered clients are third-party clients that ask for consent; metadata URLs are never fetched,jwks_uriand software statements are refused; registration access tokens (pa_rat_) forGET,PUT(rotating, with an optionalrotate_secret) andDELETE; 20 registrations per hour per network and 500 per realm, failing closed; approval queue at/client-registrations. Discovery publishesregistration_endpointonly while enabled (TestDynamicClientRegistration; dynamic-client-registration.md). - Mutual TLS (TOK-019, MCH-014, RFC 8705, migration
0056): a second TLS listener (server.mtls_listen, advertised throughserver.mtls_originasmtls_endpoint_aliases) or a trusted proxy header (server.client_cert_header); realm trust anchors with optional uploaded CRLs (/certificate-trust-anchors); client authenticationtls_client_authandself_signed_tls_client_auth; certificate-bound access tokens (tls_client_certificate_bound_access_tokens,cnf.x5t#S256) enforced by the IdP's own APIs and UserInfo; opaque tokens keepcnfand introspection returns it (TestMutualTLS; mutual-tls.md). - Workload identity federation (MCH-011..MCH-013, ADV-004, RFC 7523,
migration
0051): machine clients with a service principal accept theurn:ietf:params:oauth:grant-type:jwt-bearergrant and, when it is their only grant,token_endpoint_auth_method: none. Per-client workload trusts (/clients/{client}/workload-trusts; profilesgithub_actions,kubernetes,spiffe,generic) fix issuer, keys, exact audience, subject, claim constraints, algorithms, maximum lifetime andjtireplay control. Key sets are fetched under the federation egress rules and cached 5 minutes. The token is the client's service token and never outlives the assertion; refusals areinvalid_grantwith the reason audited asworkload.assertion.rejected. Assertions never become user identities (TestWorkloadFederation; workload-federation.md). - FAPI 2.0 security profile (ADV-001): client
security_profile: fapi2requires a confidential client authenticating withprivate_key_jwt(PS256, ES256 or EdDSA assertions) or mutual TLS, the code flow only, sender-constrained tokens,httpsredirect URIs and a realm signing algorithm of PS256, ES256 or EdDSA; it forces PAR, caps codes at 60 s and access tokens at 10 minutes, and does not rotate refresh tokens. Not certified by the OpenID Foundation; no conformance-suite run (TestFAPI2Profile; oauth-oidc-profile.md).
Authentication#
- Headless authentication transactions with continuation tokens, and a browser mode
for application-provided login UIs with cookie, CSRF token and
Originchecks (AUT-001..AUT-020, TEST-056). - Passwords (Argon2id, optional pepper, embedded and operator blocklists), email one-time codes, SMS one-time codes, magic links (fragment-carried, transaction-bound), TOTP, recovery codes with recovery-restricted sessions.
- Passkeys (WebAuthn): registration and assertion, discoverable and identified sign-in, synced-passkey counter handling, phishing-resistant profile for administrators, self-service management (PASS-001..PASS-007, TEST-025).
- Upstream federation: OpenID Connect and GitHub-style OAuth connections with PKCE, state, nonce and issuer validation; JIT provisioning; linking policies with local proof of control; tenant and email-domain restrictions; trusted upstream MFA; SSO-mandatory organizations; self-service and administrator management of linked identities (FED-*).
- Registration modes, email verification, enumeration-resistant responses,
password reset that keeps MFA (TEST-020, TEST-070), administrator-created
accounts with setup tokens, step-up and
max_age/prompthandling. - Temporary password lock and per-identifier, per-IP, per-client, per-subject and per-organization budgets stored in PostgreSQL.
- Aggregate abuse budgets per client, organization and realm for sign-up,
code/link sends and password attempts, and per-organization and per-recipient
invitation budgets (
security.rate_limits.signup_per_client,signup_per_realm,send_per_client,send_per_organization,send_per_realm,login_per_realm,invitations_per_organization,invitations_per_recipient,invitation_window; SEC-006, INV-009). - Remembered devices (AUT-013): an opt-in, random device secret (HttpOnly
pa_rdcookie orX-Remembered-Deviceheader) that can skip the MFA prompt where policy allows, without adding assurance; listed and revoked under/me/remembered-devices(TestRememberedDevices). - Concurrent browser transactions: the
pa_txcookie is scoped to each transaction's own path (AUT-016,TestConcurrentBrowserTransactions). - Profile attribute schema in realm policy (
profile.attributes: typed, validated, user-editable or admin-only) with thecomplete_profilerequired action (USR-007, AUT-018,TestProfileSchemaAndCompletion). - Transient sessions (
POST .../auth/transactions/{id}/transient, SES-020). - Pre-action hooks (EXT-009..EXT-012, EXT-017): operator HTTPS endpoints for
registration eligibility, profile validation, access-token claims enrichment and
authorization decisions, called through the egress policy with signed
requests. Hooks can only restrict; security hooks fail closed, and claims
enrichment cannot override reserved or standard user claims. Managed under
/api/v1/realms/{realm}/hooks;TestPreActionHooks(TEST-053). - Guest identities (REG-010): with realm policy
registration.allow_guests,POST .../auth/transactions/{id}/guest(a login transaction, before any identifier) creates a guest account under the sign-up budgets and signs it in withamranonandacrurn:proudauth:acr:anonymous. Guests cannot join organizations or hold roles, and transactions that need MFA or an organization stop withguest_not_permitted.POST /me/upgrade(email,password) sets a password and adds the address unverified; the same account stops being a guest once the address is verified throughPOST /verification-challenges. An address another account has verified cannot be verified, so nothing is linked or merged (TestGuestIdentities). - Registration approval queue (REG-008, migration
0021): with realm policyregistration.require_approval, self-registrations without an invitation are queued, and after email verification and required actions the transaction stops witherror: registration_pending_approval. Holders ofrealm.registrations.reviewlist the queue (GET /registration-approvals, optionalstate) and decide withPOST /registration-approvals/{id}/decision(approve,reason); applicants cannot review themselves. The applicant is emailed the decision and denied accounts are suspended; requests undecided after 14 days expire and the account is suspended, never approved (TestRegistrationApprovalQueue). - Scheduled assisted recovery (MFA-009, migration
0020): when realm policy setsrecovery.cooling_off_hours(0–168) orrecovery.require_approval,POST /users/{user}/recovery-requestsreturns202 recovery_scheduled(recovery_id,effective_at,approval_required) instead of acting at once. The owner is notified immediately and can list and cancel the request (GET /me/assisted-recoveries,POST /me/assisted-recoveries/{id}/cancel); an operator other than the requester and the owner approves it (POST /assisted-recoveries/{id}/approve;support.recovery.assist, plusrealm.users.credentials.manageforreset_mfa); the worker taskrecovery.assisted_dueruns due requests, which expire seven days aftereffective_at. Scheduling is refused for accounts without a verified email address (TestScheduledAssistedRecovery, account-recovery.md). - Authentication flow policy (AUT-012, AUT-014, SEC-014, migration
0040): realm policyauthentication_flowholds up to 50 ordered rules whose conditions (clients,organizations,methods,resources,networks/outside_networks,new_device,failed_attempts_at_least,risk_at_least,privileged,assurance_below) select an outcome:allow,require_mfa,require_phishing_resistant,challenge,reviewordeny. The decision is made once the account is known, kept in the transaction, re-made when the organization or the policy revision changes, and audited asauthentication.flow.evaluatedwith the rule and the signals used. A missing signal applies a restrictive rule unless it sayson_missing_signal: skipand never satisfiesallow.denystops the transaction witherror: "authentication_policy_denied";reviewraises the new security signalauthentication_review(and asecurity.alert).challengeadds the next actionchallenge, answered withPOST .../auth/transactions/{id}/challenge(provider_responseorcode) afterPOST .../challenge/sendfor an emailed code, or with a second factor already enrolled; wrong answers are400 invalid_proof, an unavailable provider503 unavailable. Challenge providers are operator configuration (security.challenge_providers: a siteverify-styleverify_url,site_key,secret, opt-insend_remote_ip,timeout); none is built in or contacted unless configured and named by a realm. New hook kindauthentication.riskreportslow,mediumorhighwith reasons; a failing hook makes the signal missing, never blocks by itself (TestAuthenticationFlowPolicy; headless-authentication.md). - Completing a sign-in records
user.required_actions.completed(categoryaccount) with the required actions done in that transaction (AUT-018,TestRequiredActionPermissions). - Federation adapters (FED-002, migration
0040): connections name an adapter that fixes the provider's semantics. New built-in adaptersgoogle(issuerhttps://accounts.google.com;allowed_tenantsrestricts to Workspace domains throughhd) andmicrosoft(Entra ID issuerhttps://login.microsoftonline.com/{tenant}/v2.0selected bytid,allowed_tenantsrequired, email never treated as verified, sotrusted_verified_emaillinking is refused). Operator-built binaries can register more withfederation.RegisterAdapter; an adapter whose email assertions are not trusted never yields a verified email (TestFederationAdapters; oauth-oidc-profile.md). - Upstream logout (FED-013):
/oidc/logoutresponses reportupstream_logout(not_federated,not_requested,redirect,unsupported,unavailable), and the extension parameterupstream_logout=truesends the browser on to the provider's end-session endpoint, returning through/oidc/logout/upstream-returnwithupstream_logout=returned.session.logoutaudit events recordupstream_providerandupstream_logout_requested(TestFederationAdapters; oauth-oidc-profile.md). - Verified-contact update (AUT-018): the required action
verify_contact(setting or removing it needsrealm.users.writebesidesrealm.users.required_actions) makes the next sign-in offerverify_contact;otp/send {"purpose": "verify_contact"}emails a code to the primary address even when it is verified, andotp/verifyclears the action and lists it inuser.required_actions.completed(headless-authentication.md). - Operator MFA floor (MFA-005):
security.mfa_floor(require: none|all,admin_phishing_resistant,forbid_org_admin_waiver). Realm policy updates and configuration imports below it are refused (422on the realm field), and sign-in and organization administration apply it even to policies saved before it was raised (TestOperatorMFAFloor; configuration.md). - Versioned authentication flows (AUT-019, migration
0057):/api/v1/realms/{realm}/authentication-flow/revisionssaves drafts with a reachability and bypass analysis (bypasserrors;unreachable,partial_bypass,skip_on_missinganddeny_allwarnings), simulates up to 200 sign-in cases against the live flow with the sign-in evaluator, publishes only drafts whose base is still the live flow (409otherwise) and only with acknowledged warnings, and rolls back by publishing an earlier revision as a new one. Every flow change throughPUT /settingsor configuration import is recorded as a published revision, and a bypass is refused there too (422onauthentication_flow.rules[<i>]). Auditrealm.auth_flow.*(TestAuthenticationFlowRevisions; authentication-flows.md). - Passkey attestation policy (PASS-009, migration
0059): realmwebauthn.attestation(default none: no authenticator model disclosed),webauthn.require_trusted_attestationagainst an operator FIDO MDS3 BLOB (webauthn.metadata_file, verified at startup, never fetched by the server) or realm trusted authenticators (/webauthn/trusted-authenticators), and AAGUID allow or block lists applied at registration and every sign-in. Passkeys recordattestation_trustedandattestation_source. - Advanced WebAuthn options (PASS-010): WebAuthn Level 3 related origins
behind the operator switch
webauthn.related_origins, published at/.well-known/webauthn;credProtectrequested and optionally enforced; other extensions are not requested and unrequested outputs fail the ceremony (TestPasskeyAttestationTrust,TestPasskeyAdvancedCapabilities; passkey-trust.md). - Federation transfer (FED-016, migration
0080):/federation-transfersmoves account links to another connection or a new upstream tenant through a reviewed map (orsame_subjectmode) with per-entry analysis and collision reporting, approval by a second administrator, and an apply that re-analyses under lock, keeps link IDs and drops stored upstream tokens; never by email (TestFederationTransfer; federation-transfer.md). - Risk integrations (ADV-012) are documented as the
authentication.riskhook contract with its deterministic fallback; a risk verdict cannot override an explicitdenyrule (TestRiskNeverOverridesDeny; authentication-flows.md). - HOTP (MFA-013, migration
0049): counter-based codes (RFC 4226, SHA-1, 6 digits), opt-in per realm (hotpinmfa.allowed_factors; realm policymfa.hotp.look_ahead0 = 10, at most 50, andresync_window0 = 100, at most 1000). Self-service/me/hotp/enroll,/activate,/resync(two consecutive codes, 5 attempts per 15 minutes) andDELETE /me/hotp; one active token per account. Sign-in actionhotp, offered wheretotpis (not under the phishing-resistant profile); a code is accepted once, with the counter advanced under a row lock and a conditional update. The factor counts as MFA (amrhotp), restores recovery-restricted accounts at sign-in and allows remembered devices; an administrator's factor reset revokes it; a realm that stops allowing it stops offering and accepting it (MFA-005).identity-cli loginprompts for it (TestHOTP; headless-authentication.md). - X.509 certificate sign-in (PROV-016, migration
0056): realm policylogin.certificatesoffers thecertificateaction; the certificate comes from the mutual-TLS listener or a trusted proxy and must chain to a realmusertrust anchor; accounts are resolved by administrator bindings (/users/{user}/certificate-bindings) or a single verified SAN e-mail address; failures are proof failures audited with a reason. Amulti_factoranchor makes the sign-in phishing-resistant MFA. Organizationallowed_login_methodsand flow rulemethodsacceptcertificate(TestCertificateSignIn; mutual-tls.md).
Accounts, organizations and authorization#
Organization membership metadata (USR-009):
PATCH /organizations/{org}/members/{membership}acceptsmetadata(merged; anullvalue removes a key; at mostlimits.metadata_bytesas JSON; together with or withoutstate), for callers holdingorg.members.writewithin the member-management ceiling. The memberGET, list andPATCHreturn it only to such callers; it is not the user's profile,admin_metadataor internal metadata, never appears in the member's own views, personal data export or tokens, and cannot be read or written through another organization. Changes are audited asorganization.membership.metadata_updatedwith the key names only; the realm data export carries it; it is cleared when the membership is removed or expires, when the organization is deleted and when the account is deleted (TestMembershipMetadata; data-inventory.md).Self-service profile, identifiers with verification challenges, sessions, consents, personal access tokens, data export and deletion request.
Organizations with memberships, invitations (rotated secrets, non-consuming inspect, inviter re-validation), join requests, groups, DNS domain verification, ownership transfer, last-owner protection and asynchronous deletion (TEST-011, TEST-015..TEST-019, TEST-064).
Scoped RBAC with built-in roles, delegation ceilings, composite roles and membership-scoped assignments; policy decision API (single and batch) with obligations (TEST-010, TEST-014).
Support impersonation with three-level enablement, approvals, opaque short-lived tokens validated on every introspection, handoff codes, customer visibility and opt-out (IMP-001..IMP-024, TEST-040..TEST-047, TEST-071).
Service principals, API keys, personal access tokens, credential inventory and expiry notifications (TEST-065).
Account activity API
GET /me/activity: recent sign-ins, sessions, authenticators, security changes and support access, without other principals' identifiers (USR-019,TestAccountActivity).Device labels derived from the User-Agent for sessions (
device_label), and administrator revocation of a single session (DELETE /users/{user}/sessions/{id}); realm policysessions.max_concurrentcan lower the deployment limit (TestSessionLimitsAndAdminRevocation).Invitations: bulk creation with per-item results (
POST /organizations/{org}/invitations/batch, up tolimits.max_batch_size, 100 by default), administrator reassignment (.../invitations/{id}/reassign), binding to the account that held the invited address so a recycled address does not inherit it (migration0014), and project-access invitations backed by project grants (/projects/{project}/grants) (INV-011, INV-012, INV-014, USR-021,TestBulkAndProjectInvitations).Membership states
invitedandpending_approval, and expiry of join requests after 14 days (TEN-003,TestMembershipLifecycle).Join request review (INV-013): a new join request emails up to 20 users who hold
org.join_requests.reviewin the organization (templatejoin_request), without naming the requester.POST /organizations/{org}/join-requests/{id}/reviewers(org.members.write;reviewer_ids: 1–10 users who holdorg.join_requests.review, never the requester) assigns a pending request: the assignees are notified, and only they or a reviewer who also holdsorg.members.writecan decide it. The requester is emailed the decision (templatejoin_request_decision), lists their own requests atGET /me/join-requests(reviewers are never shown) and withdraws a pending one withPOST /organizations/{org}/join-requests/{id}/withdraw, which releases thepending_approvalmembership. Auditedorganization.join_request.reviewers_assignedand.withdrawn(TestMembershipLifecycle; ADR 0006).Domain re-verification (TEN-009, migration
0030): verifying a domain now keeps a digest of the accepted DNS TXT proof, and the hourly worker taskdomains.recheck(up to 100 due domains per run) looks the record up again every realm policydomains.recheck_days(1–365; 0 or omitted means 30). A missing proof is retried every 24 hours; afterdomains.lapse_after_failuresconsecutive failures (1–30; 0 or omitted means- the domain becomes
expiredand stops counting as verified. Auditedorganization.domain.rechecked(categoryconfiguration) andorganization.domain.verification_lapsed. A lapsed domain is verified again by removing and re-adding it. Domains verified before migration0030have no stored proof and are not re-checked (TestDomainReverification; identifiers.md).
- the domain becomes
Organization aliases and subdomain routing (TEN-011, migration
0034): renaming an organization keeps its former slug as an alias, andPOST /organizations/{org}/aliases {"alias": "..."}(org.update; slug syntax) adds one;GET .../aliases(org.read) lists them andDELETE .../aliases/{alias}releases one. Aliases resolve wherever an organization is named by slug (theorganizationparameter of authorization and device requests, organization references in user imports), and no other organization can take an alias as its slug or alias (409). Auditedorganization.alias.added/.removed(categoryconfiguration).PATCH /organizations/{org}/domains/{id} {"include_subdomains": true}(org.domains.write; auditedorganization.domain.updated) lets a verified domain also route addresses of its subdomains. Email-domain routing (at theidentifystep and for organization registration rules) is deterministic: an exact verified domain wins, otherwise the longest verified parent domain withinclude_subdomains, and only active organizations route (TestOrganizationAliasesAndDomainRouting; identifiers.md).Organization templates (TEN-017, migration
0032):POST /organization-templates(realm.organizations.write) stores a new version (numbered from 1, never changed) of a named template (name: 1–63 lowercase letters, digits and dashes;description;documentwith an organizationpolicy, up to 50 customroleswith 1–100permissionsand norealm.orsupport.permissions, up to 50groupswith up to 20role_names, up to 20service_principals(namematching^[a-z0-9][a-z0-9-]{0,62}$,description,role_names) and up to 20webhooks(name, anhttps://destination_url,event_types,payload_form)).GET /organization-templates(realm.organizations.read;?name=for one template) lists versions, newest first.POST /organizations/{org}/template-applications(org.update;template, optionalversion(default: the latest) anddry_run) returns the plan (create,update,unchanged,conflicts,applied) and, withoutdry_run, applies it in one transaction: a differing policy replaces the organization's policy, missing roles and groups are created and missing group role grants assigned with the caller's authority (delegation ceilings apply), and existing items are left alone, so re-applying is idempotent. A role that exists with other permissions is a conflict and is never rewritten; any conflict fails the application with409andconflicts. Service principals are created as<organization slug>-<name>, owned by the organization, allowed only for it, with their roles assigned in its scope and no credentials; webhook subscriptions are created for the organization, limited to public addresses, and the response'swebhook_secretsmaps each new subscription's name to its signing secret, shown once and never replayed for anIdempotency-Key. A template with service principals or webhooks needsorg.service_principals.writeororg.integrations.writein the organization; without it the plan lists a conflict. Event types, payload form and destination are validated only when applying, and an existing service principal or subscription of the same name is reported unchanged without comparing its settings. Auditedorganization.template.savedandorganization.template.applied(categoryconfiguration) (TestOrganizationTemplates; ADR 0006).Dynamic groups (TEN-014, migration
0033):PUT /organizations/{org}/groups/{group}/dynamic-rule(org.groups.write) with{"all": [{"attribute": "...", "equals": "..."}]}(1–5 conditions, each with exactly one ofequalsorin(at most 20 values)) derives the group's members from realm profile attributes: active members of the organization whose active account matches every condition, at most 10,000. Only attributes of the realm'sprofile.attributesthat are notuser_editableare accepted (422), so nobody joins a group, or gains its roles, by editing their own profile; the caller must be able to grant the group's roles. Setting a rule removes manually added members and recomputes at once;GET(org.groups.read) returnsrule,recomputed_at,member_countanderror, andDELETEmakes the group static again (its rule-derived members stay listed but no longer grant the group's roles; see Known limitations). Members of a dynamic group cannot be added or removed by hand (409). The worker taskgroups.dynamic(every 10 minutes, up to 100 groups per run, least recently recomputed first) applies attribute changes; a rule that no longer validates (an attribute was removed or became user-editable) removes every rule-derived member and recordserror. Changes bump the membership's authorization revision. Setting or removing a rule is audited (group.dynamic_rule.set), and so is every member a rule adds or removes (group.member.added/.removedwithsource: "dynamic", actor the administrator who set the rule orsystemfor scheduled runs) and every manual member removed when a rule is set (source: "dynamic_rule_set"). Rule-derived memberships grant roles only while the group was recomputed withinsecurity.dynamic_group_max_staleness(default1h) and its rule has no error (AZ-010) (TestDynamicGroups; ADR 0006).Organization
registration_mode(inherit,closed,invitation_only,domain_restricted) that can only tighten the realm mode (REG-001,TestOrganizationRegistrationMode).Organization-scoped identity providers that organization administrators manage under
/organizations/{org}/identity-providers(TEN-007).ABAC policies that restrict RBAC (deny or obligations only): versioned, approved, published, diffed and rolled back under
/policies, evaluated with missing attributes failing closed; policy simulation atPOST /authorization/simulate; conditional role assignments; two-party approval for roles markedrequires_approval(/role-assignment-requests) (AZ-012..AZ-019,TestPoliciesAndConditionalGrants).Decision audit sampling (AZ-020):
/authorization/checkand/check-batchrecordauthorization.decision(categoryauthorization) for denials, privileged actions and support tokens; realm policydata_access.sample_allowed_decisions_percent(0–100, default 0) also records that share of the other allowed decisions, markedmetadata.sampled: true. Every refused authenticated API request is recorded once asauthorization.denied(reasonPERMISSION_REQUIRED; metadatamethodandroute, the path with IDs replaced by{id}, pluspermissionwhen one permission was missing andconcealedwhen the organization's existence was hidden with 404; never the body or query) (TestDecisionAuditSampling; ADR 0013).Decision cache contract (AZ-019): decisions of
/authorization/checkand/check-batchmade by RBAC alone (aNOT_PERMITTEDdenial, or an allow of a non-privileged action with no applicable ABAC policy, active pre-action hook or obligation; never for support tokens) carrycache_key(SHA-256 over realm, subject, actor, client, organization, resource, action, the subject's authorization revision, the policy version and the asserted context) andcache_ttl_secondsfrom the newsecurity.decision_cache_ttl(default30s,0disables); every other decision hascache_ttl_seconds: 0(TestDecisionCacheContract; ADR 0013). The SDKs do not cache decisions yet; see Known limitations for what the key does not cover.Support capability conditions on operator role assignments (organizations, target categories, support modes and scopes), re-checked on every use of a live grant (IMP-002, IMP-007,
TestSupportCapabilityProfile).Authorization inspection with provenance:
GET /subjects/{subject}/permissions,GET /me/permissionsand organization-scopedGET /organizations/{org}/members/{membership}/permissions, with realm sources and policies redacted where the caller may not see them (AZ-009,TestAuthorizationInspection).Account merges (USR-011, migration
0022): an administrator withrealm.users.writeproposesPOST /users/{user}/merges(source_user_id,reasonof 10+ characters; both accounts active, registered (not guests) and not protected). The stored plan lists identifiers and upstream links to move, duplicate identifiers to drop, memberships to move with the plain member role, the source's roles for review (never copied) and credentials that are not transferred. Both owners are notified and each confirms with recent authentication (GET /me/account-merges,POST /me/account-merges/{id}/confirm); then an administrator runsPOST /account-merges/{id}/execute, which re-plans against current state and, in one transaction, moves the data, revokes the survivor's sessions, deletes the source account, records its ID as a subject alias (GET /subject-aliases/{id},realm.users.read) and emits theuser.mergedevent. Requests can be confirmed or executed for 7 days (TestAccountMerge).Customer-approved support access (IMP-022): when the realm's
impersonation.approvalor the organization'simpersonation_approvaliscustomer, the target user is emailed at request time, sees pending requests (reason and ticket, not the operator) atGET /me/impersonation-requests, and approves withPOST /impersonation-requests/{id}/approveor refuses withPOST /impersonation-requests/{id}/denyusing an account-audience token. The deny route also lets the requester withdraw and lets policy approvers refuse (TestCustomerApprovedSupportAccess).Personal access token ceilings (MCH-006): scopes are limited at creation to the owner's current authority and narrowed again on every introspection, and organization policy
personal_tokens: "disabled"refuses organization-bound tokens whilepersonal_token_max_dayscaps their lifetime (TestPersonalTokenCeiling). Credential inventory flagsowner_departed,over_privilegedandno_expiry(client secrets) besidesexpiring,unusedandownerless, andPOST /credential-inventory/revocationsrevokes up to 100 API keys, personal tokens or client secrets with per-item results (MCH-007, MCH-016, APP-014,TestCredentialInventory; machine-clients.md).Service-principal owner departure (MCH-007, migration
0031): the hourly worker taskmachine.owner_departuresfinds active service principals whoseowner_user_idis no longer an active user (for example suspended, locked or deleted), records when that was detected and auditsservice_principal.owner_departedonce (subject: the owner). Realm policymachine.owner_departureflag(default) only reports;disabledisables the principalmachine.owner_departure_grace_days(0–90, default 0) after the detection, with the effects ofPOST /service-principals/{id}/disable(API keys and opaque tokens revoked,service_principal.disabledwith reason codeowner_departed,service_principal.state_changedevent), at most 100 per realm and run. An owner who is active again clears the marker (TestServiceOwnerDeparture; machine-clients.md).Account lifecycle jobs (USR-015, migration
0027): realm policylifecycle.dormant_days(0 disables, at most 3650) withlifecycle.dormant_actionreport(default; audituser.dormant_detectedonce per dormancy) orsuspend, andlifecycle.guest_ttl_days(default 30) for deleting guest accounts unused that long. Administrators schedule a suspension withPUT /users/{user}/scheduled-deactivation {"at", "reason"}and cancel it withDELETE(realm.users.suspend; auditeduser.deactivation_scheduled/user.deactivation_cancelled); administrator user views showdeactivate_at. The worker tasklifecycle.users(every 10 minutes) applies these rules and the expiry of unverified registrations (lifecycle.unverified_account_ttl_hours) to at most 100 accounts per rule and realm per run, re-checking each account in its own transaction;GET /lifecycle/preview(realm.users.read) is the dry run. Protected accounts are never treated as dormant (TestUserLifecycleJobs; data-inventory.md).Bulk user operations (USR-016):
POST /user-bulk-operationswithaction(suspend,reactivate,lock,revoke_sessionsoradd_required_actionwithrequired_action), 1–1000 uniqueuser_ids, areasonof 5–500 characters and optionaldry_runqueues auser.bulkjob. It needs an MFA session and the action's permission (realm.users.suspend,realm.users.sessions.revokeorrealm.users.required_actions), re-checked when the job runs; anIdempotency-Keyreturns the same job. Each account is handled in its own transaction with a checkpoint every 50 records; protected accounts are skipped (except forrevoke_sessions), and so is the requester's own account forsuspendandlock. The job result lists onlyuser_id,outcome(applied,valid,skipped,failed) andcodeper account (TestBulkUserOperations; exit-and-export.md).External IDs (USR-017):
GET /users/{user}/external-ids(realm.users.read),PUT /users/{user}/external-ids/{source} {"external_id": "..."}andDELETE(realm.users.write; auditeduser.external_id.set/.removedwith the source system only), and the lookupGET /external-ids/{source}/{id}(realm.users.read). An account holds one ID per source system (names matching^[a-z0-9][a-z0-9._-]{0,62}$, values of 1–256 printable characters); IDs are unique within a source system only and share theimport_mappingstable with user imports (TestExternalIDs; data-inventory.md).Identity read evidence (USR-018, AUD-015): with realm policy
data_access.log_identity_reads,GET /users/{user}recordsuser.readandGET /usersrecordsuser.search(withresult_countandfiltered) in the audit categorydata_access, with the actor, the account (for reads) and whether the view wasmasked; the event is written before the data is returned, and the request fails if it cannot be written. Support operators with onlysupport.users.readkeep receiving masked views. This replaces the earlieruser.searchevidence, which was recorded only for holders ofrealm.users.sensitive.read(TestIdentityReadEvidence).Consent management (APP-013, migration
0029): clients takeconsent_terms_version(at most 64 characters). A consent records the terms version it was given for and covers the client only while that version is current, so changing the version asks users again at their next authorization (existing grants are not revoked); theconsentaction'soptionscarryterms_version. Administrators see a user's consents withGET /users/{user}/consents(realm.users.read;?history=trueadds revoked ones) and revoke one withDELETE /users/{user}/consents/{id}(realm.users.write), which also revokes the client's grants for that user. Pre-authorization:POST /clients/{client}/consent-preauthorizations(realm.clients.write;scopes: 1–50 existing scopes, neveroffline_access;reasonof at least 5 characters; optionalorganization_id) consents on behalf of the realm or one organization for the client's current terms version. Users signing in through that organization (or any, for realm-wide ones) are not asked when a pre-authorization covers every requested scope, and a consent withmode: preauthorizedis recorded.GETlists a client's pre-authorizations (realm.clients.read), andDELETE .../consent-preauthorizations/{id}revokes one together with every consent and grant that relied on it. Auditedconsent.preauthorizedandconsent.preauthorization_revoked(categoryconfiguration) andconsent.revoked(TestConsentManagement; headless-authentication.md).Organization deletion workflow (TEN-024): the deletion preview also counts role assignments, groups, verified domains, owned service principals, active automation credentials, webhook subscriptions, identity providers, the mail server, consent pre-authorizations, export results, pending jobs, retained audit events and active legal holds, and lists what is retained. Starting a deletion now also disables the organization's service principals, webhook subscriptions and identity providers, revokes its consent pre-authorizations, erases its job results and cancels its other jobs; finishing it deletes its notification settings (mail server and password) and webhook subscriptions and erases identity-provider client secrets (
TestOrganizationDeletionWorkflow; data-inventory.md).Deletion propagation report (PRV-005):
GET /users/{user}/deletion-status(realm.users.read) shows, for a deleted account, the state of each place its data lived (primary storage, sessions and grants, exports, notifications, connectors, imported audit history, caches, audit evidence, backups), derived from current data (TestDeletionPropagationStatus; data-inventory.md).SCIM 2.0 inbound provisioning (PROV-001..PROV-009, PROV-018, TEN-015, migration
0042;internal/provisioning,TestSCIMProvisioning): organization administrators holdingorg.integrations.write(built-inowner, notadmin) manage connectors at/organizations/{org}/provisioning-connectors(writes from an MFA session;PUTwithIf-Match;POST .../{id}/tokenrotates the bearer token, which is shown once and stored as a digest). A connector'srole_ceiling_idsmust be within its creator's delegation ceiling, and itsgroup_role_mappingsanddefault_role_idwithin that ceiling. The SCIM endpoint/api/v1/realms/{realm}/scim/v2servesServiceProviderConfig,ResourceTypes,Schemas,UsersandGroups(list with filters,attributes/excludedAttributesand paging, create, read,PUT,PATCH,DELETE) with ETags, 1200 requests per minute per connector, and SCIM error documents; bulk, sort and password change are not advertised. A connector sees only what it provisioned (its ownexternalIdnamespace), adopts an existing account only when its verified email is in a domain the organization verified (never protected accounts or holders of realm-scoped roles), changes identity attributes only of accounts it created that belong to no other organization (400 mutabilityotherwise), and treatsactiveandDELETEas the organization membership's state, never the account's. Group roles come only from approved mappings. Each connector reportslast_sync_at,last_errorand per-operationstats. Audit:provisioning.connector.*,scim.user.provisioned/.updated/.deprovisioned(scim-provisioning.md, incidents.md).Ownership transfer (USR-020, APP-001, APP-014,
TestDeletionNeedsOwnershipTransfer):POST /users/{user}/ownership-transfer {"to_user_id"}moves every project, client, service principal, resource and identity provider an account owns to another account of the realm in stateactiveorrecovery_restricted(it needs the write permission of each kind) and answers{"transferred": {"<kind>": <count>}}. Each move is audited as<kind>.ownership_transferred(categoryconfiguration,previous_owner_user_id,owner_user_id). A single project still moves withPOST /projects/{project}/ownership-transfer(owner_user_idand/orowner_organization_id,reason;project.ownership_transferred). See Changed for the deletion rule (data-inventory.md).Conditional updates of settings and grants (API-010, DB-010,
TestConditionalSettingsUpdates):GET /clients/{client}/resource-grants/{resource}(new,realm.clients.read) returns the grant with a contentETag(W/"g...") thatPUTaccepts asIf-Match;GET /notification-settingsreturnsW/"c..."forPATCH;GETandPUT /organizations/{org}/notification-settings/smtpreturn anETagthatPUTaccepts;POST /roles/{role}/permissionsandPOST /organizations/{org}/roles/{role}/permissionsaccept the role version (W/"vN"; the realm route returns the newETag).If-Matchis optional on all of them; a stale value is412 precondition_failed(oauth-oidc-profile.md, ADR 0016).Support write allowlist on the decision API (IMP-011): resources take
impersonation_policy.write_actions(OpenAPI and configuration document); see Security fixes (resource-servers.md).GET /organizations/{org}/invitations/{id}(org.invites.read) returns one invitation with itsETag; invited users decline withPOST /invitations/decline {"invitation": "<secret>"}(204, auditedinvitation.declined).Organization relationships (TEN-021, migration
0081):parent_childandpartnerrelationships between organizations of a realm, proposed by organization administrators and accepted by the other side, or recorded directly by realm administrators; one parent per child, no cycles, at most 10 levels. A relationship grants no membership, role or permission. New permissionsorg.relationships.read,org.relationships.writeandorg.projects.share.Partner project access (TEN-020): an owner organization shares a subset of a project's roles with a related organization (
/organizations/{org}/projects/{project}/grants), which assigns only that subset to its active members (/organizations/{org}/project-role-assignments,403 grant_ceiling_exceededbeyond it). Revoking the grant or removing the member removes those assignments (TestOrganizationRelationshipsAndPartnerProjects; partner-access.md).Relationship-based authorization (AZ-021, AZ-022, AZ-023, AZ-027, migration
0053): resource types (/resource-types/{name}) with relations,implied_by, one-hop links and the application permissions each relation gives; relationship tuples per organization (/organizations/{org}/relationships, up to 100 atomic writes, subjects confined to the organization); object-level decisions in/authorization/checkwhen RBAC does not grant, bounded at depth 6, 64 object reads and 1000 tuples per object, never cacheable;POST /authorization/list-objectsreturning only verified objects (orall: true); user-managed sharing (/me/shares). New permissionsrealm.resource_types.read/writeandrealm.relationships.read/writefor the built-inresource-serverrole (the read ones also forrealm-auditor) (TestRelationshipAuthorization; relationship-authorization.md).External policy engines (AZ-026): operator-configured
security.policy_engines(HTTPS, bearer token, timeout at most 5 s) that a realm names in its policypolicy_engine(restrict, the default, ordecide;on_errordenyorlocal). The decision API sends the subject, action, resource, context and local result; accepts native or OPA data API answers; denies withEXTERNAL_POLICY_DENIEDorEXTERNAL_POLICY_UNAVAILABLE; and never marks such decisions cacheable (TestExternalPolicyEngine; policy-engines.md).Just-in-time elevation (AZ-024, migration
0054): eligibilities (/elevation-eligibilities, 5 minutes to 7 days, approval required for privileged roles), requests from a recent MFA session with a reason (/me/elevations), approval by someone else who could assign the role, and an ordinary expiring role assignment (source: elevation) removed by the expiry sweep (TestJustInTimeElevation; elevation-and-access-reviews.md).Access reviews (AZ-025, migration
0055): campaigns that snapshot up to 10000 role assignments of the realm or one organization, named reviewers who keep or revoke items (never their own), closing by an administrator or at the due date that applies revocations and records each outcome, and an evidence export that "documents an access review; it is not a regulatory certification". New privileged permissionrealm.access_reviews.manage(TestAccessReviews; elevation-and-access-reviews.md).Agents (MCH-017): service principals with
is_agent: trueneed an accountable human owner; assigning them a privileged role (including through composition and custom permissions registered as privileged) answers202 pending_approvaland needs a second administrator; their tokens carry the reserved claimagent: true(also in introspection), andact.agentmarks exchanges they perform (TestAgentPrincipals; machine-clients.md).Ownership report (DOM-009, APP-014, MCH-007, migration
0047):GET /ownership-reportlists live clients, projects, service principals, resources, identity providers, webhooks, hooks and authorization policies that areownerlessorowner_departed;POST /policiesandPOST /hooksacceptowner_user_id(default the creating user) (TestOwnershipReport; machine-clients.md).
Evidence, delivery and integrations#
- Audit events written in the same transaction as each change, append-only
trigger, redaction, hash-linked batches, verification (
identity-cli verify-audit, API), scoped query and NDJSON export. - Transactional outbox; signed webhooks with retries, dead-lettering, replay and secret rotation with 24 h dual signing (TEST-054, TEST-058); SSRF-restricted egress for webhooks, back-channel logout, SMS gateway and federation (TEST-052).
- Email over SMTP; SMS through an operator-run HTTPS gateway with signed requests, a country allowlist and a daily budget; development sinks; per-realm and per-organization templates and sender settings.
- Asynchronous jobs: user, audit and configuration exports with single-use
download tokens (TEST-069); bounded user import with dry run (submitted records
encrypted and erased after the job, migration
0007); configuration-as-code validate, import and export. - Signed audit batches (Ed25519 key derived from the root key) and
retention-aware verification: pruned batches are recorded in the append-only
audit_prunesledger (migration0011) and still verify;identity-cli verify-audit -require-signatures(AUD-008, AUD-009,TestAuditSignaturesAndRetention). - Audit search dimensions (AUD-011):
GET /audit-eventsandGET /organizations/{org}/audit-eventsalso filter bygrant_id,session_id(the event's session or the actor's session),target_id,categoryandnetwork: an IP address or CIDR prefix, matched by overlap with the truncated network context stored with each event, so an address finds the events of its /24 or /48 (or exact matches whenprivacy.store_full_ipis set) (TestAuditSearchDimensions; monitoring.md). - Session-event stream
GET /session-events(SES-019): gap-free, ordered by commit horizon (migration0012), long-poll with a resumable cursor, 7-day history (TestSessionEventStream). - Delivery feedback, sender control and notification preferences
(migration
0019,TestMessagingControls):POST /delivery-eventsaccepts up to 500delivered,bounceandcomplaintevents from the email provider, signed with Standard Webhooks headers underdelivery.email.events_secret(the route answers404 feature_disabledwithout a secret). Permanent bounces and complaints suppress the address for optional notices; a bounce marks the job with thatprovider_message_idfailed. Feedback never verifies or changes an identity (MSG-007).GET/PUT /me/notification-preferences: users can mute optional notices (currently the new-login alert); security and account notices cannot be muted (422) and are delivered even to suppressed addresses (MSG-010).- A realm sender address must use a domain listed in
delivery.email.allowed_sender_domainsor the domain ofdelivery.email.from; an organization sender must use one of the organization's verified domains (MSG-005). POST /notification-settings/check(realm.notifications.write) tests the SMTP connection, TLS and authentication without sending a message and reports only the failing stage (MSG-012).
- Localized notices (MSG-006): a notice addressed to an account is written
in the account's
localewhen the message sets none (without either, the realm's or organization's default locale; templates then fall back to the base language and to English), and itstime,expiresandscheduledvalues are rendered in the account'szoneinfotime zone (for example2 Jan 2026 21:04 JST). Messages without an account, and accounts without a valid time zone, use UTC (TestNoticeTimeZones; ADR 0016). - Organization mail servers (TEN-022):
PUT /organizations/{org}/notification-settings/smtpwithhost,port(25, 465, 587 or 2525),tls_mode(starttlsorimplicit; plain text is refused) and optionalusernameandpasswordsends the organization's email through its own SMTP server. It needs the new privileged permissionorg.integrations.write(held by the organizationownerrole, not byadmin) and, for users, an MFA session. The password is encrypted (AES-256-GCM, bound to the realm and organization, so it decrypts only for that organization's mail), is never returned (GET, which also needsorg.integrations.write, showshas_password) and is kept when a laterPUTomits it. Every resolved address of the server must pass the delivery egress policy (delivery.egress_allowlistfor private addresses) at connection time.DELETEreturns the organization to the deployment relay. Auditednotification.smtp.updated(host, port, TLS mode and whether the password changed) andnotification.smtp.cleared(categoryconfiguration). The development sink still takes precedence, andPOST /notification-settings/checktests only the deployment relay. Upstream identity-provider client secrets (including organization-scoped connections) and webhook signing secrets are likewise encrypted with their connection's or subscription's ID as associated data, so each decrypts only for its own integration. SCIM connector tokens are stored only as SHA-256 digests (TestTenantSMTPCredentials; ADR 0016). - Audit retention by category and legal holds (AUD-009, migration
0035):privacy.audit_retention_by_categorymaps audit categories to their own retention (each at least24h), overridingprivacy.audit_retentionfor those categories;emergencyis refused because emergency events are never pruned, and so is any name that is not an audit category (see Changed). Legal holds:POST /audit-legal-holds(realm.audit.legal_hold, held byrealm-admin) with areasonof 5–500 characters and optionalorganization_idandsubject_idkeeps every matching event of the realm (all events, the organization's events, and/or events whose subject or actor issubject_id) from being pruned untilPOST /audit-legal-holds/{id}/release;GET /audit-legal-holdslists the realm's holds, active first. Auditedaudit.legal_hold.placedandaudit.legal_hold.released(categoryaudit; a release raises theconfiguration_changesecurity signal) (TestAuditRetentionCategoriesAndHolds; configuration.md). - Audit archive (AUD-008, migration
0037): withprivacy.audit_archive_dirset, the worker taskaudit.archive(every minute, up to 500 batches per run) writes every sealed batch after the last archived one to that directory as a write-once, read-only filebatch-<12-digit sequence>.ndjson: a header line with the batch hash, chain link, signature, event count and, for batches pruned before the archive existed,archived_events, then the batch's events as recorded. An existing file is accepted only if it is byte-identical. While an archive is configured, retention prunes only batches already archived. After a database restore, a batch that reuses an archived sequence number with other content starts a fork directoryfork-<sequence>-<batch id>/where archiving continues (migration0039, operator eventaudit.archive.forked). Mount storage you control independently there, ideally with retention locks.identity-cli verify-audit-archive [-dir <path>]checks the directory and its forks without the database (it reads the configuration only for the keys): event content hashes, batch digests, batch hashes, the hash links between files, missing sequence numbers, the first batch's link, event counts againstevent_countandarchived_events, each fork's link to the main archive, and batch signatures (always required); it reportsfirst_sequence,last_sequenceandforks, and exits non-zero on problems (TestAuditArchive; incidents.md). - Reported application activity (AUD-013): a service principal holding
realm.activity.reportrecords application activity in the realm's audit log withPOST /activity-events:type(app.followed by up to 96 lowercase letters, digits, dots and underscores), optionaloccurred_at(within the last 24 hours and at most 5 minutes ahead),organization_id(an existing organization),subject(userorservice_principal),outcome(successby default,failureordenied) andmetadata(at most 4 KiB, redacted like server metadata). Reported events carrysource: application, categoryapplicationandreporting_service(the caller's client ID), so they are told apart from server-generated evidence, and are sealed into the chain like any event. The response is202withid,sourceandreporting_service; user tokens are refused (403) (TestReportedApplicationActivity; monitoring.md). - Security alerts (AUD-012): every audit event of a realm that carries a
security signal is also published, in the same transaction, as the webhook
event
security.alertwith identifiers only (signal,audit_event_id,event_type,actor_type,actor_id, optionalsubject_typeandsubject_id). New signals:protected_account_signin(a protected account signed in),privileged_grant(an assignment of an owner role, a realm role, or a role holding a permission the catalog marksprivileged, directly or through a role it composes; not the built-in organizationmemberandadminroles) andconfiguration_change(realm.policy.updated,federation.provider.*,hook.created,hook.updated,webhook.created,audit.legal_hold.released); they are counted inproudauth_security_signals_totaltoo. Both payload forms carry every member of the alert, andGET /audit-events?id=<audit_event_id>(also on the organization route) returns the audit event it refers to (TestSecurityAlerts; api/events/README.md). - Webhook backlog (EXT-005):
GET /webhooks/{id}/backlog(realm.webhooks.read) returns the subscription's delivery counts (pending,retrying,dead_letter,delivered),oldest_due_seconds(how long the oldest due pending or retrying delivery has waited),last_delivered_atandlast_failure_category(of the newest retrying or dead-lettered delivery) (TestWebhooksAndBackchannelLogout; webhooks.md). - Hook diagnostics (EXT-017, migration
0036): every pre-action hook call is recorded inhook_invocationswith its outcome, latency and a coarseerror_category(statuswithstatus_code,timeout,egress_denied,malformed_response,unreachableorerror), never the payload or response;retention.cleanupremoves records older than 7 days.GET /hooks/{id}/diagnostics(realm.hooks.read) returns the last 100 invocations withrecent_invocations,recent_failures,latency_p50_ms,latency_p95_msandhealth(unknownwithout calls,degradedwhen more than 10 % of them failed, otherwisehealthy) (TestPreActionHooks; monitoring.md). - Audit corrections (AUD-007):
POST /audit-events/{id}/corrections(realm.audit.correct, privileged;reason5–1000 characters, optionalcorrectionobject of at most 8 KiB) recordsaudit.correctionlinked to the original (targetaudit_event, metadatacorrects_event_id,corrects_type,reason,correction) and answers201 {"event_id", "corrects_event_id"}; the original never changes. Find them withGET /audit-events?type=audit.correction&target_id=<id>(TestAuditCorrections; data-inventory.md). - Scoped audit retention (AUD-009): realm policy
audit.retention_daysand organization policyaudit_retention_days(30–3650) keep that scope's evidence longer; an event is pruned only when every retention that applies (category or deployment, realm, organization) has passed and no legal hold covers it (data-inventory.md). - Lifecycle workflows (EXT-015, EXT-016, migration
0070): realm-scoped, versioned definitions (schema_version1,api/workflows/definition.v1.json) with event triggers (eight user and membership event types), schedule triggers (fixed selectors of role holders or organization members, at most 500 runs per firing) and manual starts, and a bounded step vocabulary (wait,approval,assign_role,add_to_group,remove_access,notifywith fixed notice codes and the new mandatoryworkflow_noticetemplate,call_webhook,suspend_user,end); no expressions, conditions or loops. Revisions are immutable and are approved, published and rolled back; the publisher is the accountable actor, checked against the delegation ceiling at publication and again inside every effect's transaction. Runs are durable and leased by the new worker tasksworkflows.runandworkflows.schedule; every step attempt is recorded with its idempotency key, input version, result, retry policy and compensation strategy, internal effects commit atomically with the attempt, endpoint calls carry a stableIdempotency-Key, and failed runs compensate (undo their own grants, notify endpoints). Approvals by named approvers through/me/workflow-approvals. New permissionsrealm.workflows.read,.write,.publishand.run; realm policyextensions.workflow_publish_approval;GET /workflows/{id}/diagnostics; disabling a workflow is audited in categoryemergency; events a run causes carry the run ID ascorrelation_id(TestWorkflowDelegationCeiling,TestWorkflowIdempotency,TestWorkflowScheduleAndOffboardingand others; workflows.md). - Plugins (EXT-014, migration
0071): signed configuration bundles of pre-action hooks, webhook subscriptions and workflows. Packages carry an Ed25519 signature over the manifest (domainproudauth-plugin-v1\n), which binds the payload by SHA-256, by a publisher key the operator lists in the new configurationextensions.plugin_publishers(no unsigned mode). Upload, second-administrator review (realm policyextensions.plugin_review,second_adminby default), install, upgrade, downgrade, rollback, enable, disable (an emergency control) and uninstall, with declared permissions checked for uploader, reviewer and installer, dependency checks, compatibility checks against the extension contracts, and parts reconciled by name so endpoints keep their signing secrets; ordinary edits of plugin parts answer409. New permissionsrealm.plugins.read,.manageand.approve(TestPluginLifecycle,TestPluginSingleAdminReview; plugins.md). - Extension contract versions (EXT-018, migration
0072): webhook events, the pre-action hook contract, plugin manifests and workflow definitions are versioned independently of each other and of releases;GET /extension-contractslists the versions. Webhook subscriptions chooseevent_version1 (default; byte-for-byte the previous envelope) or 2 (envelope withorganization_idinstead oforg_idand apayload_formmember;api/events/envelope.v2.jsonand<type>.v2.json); hooks pincontract_version(1), sent as theProudAuth-Hook-Versionheader, and a response echoing another version fails as the hook kind fails; JSON Schemas for hook requests and responses inapi/hooks. A version stays supported at least 12 months after its successor (TestContractVersions; extension-contracts.md). - No embedded code execution (EXT-013): ProudAuth ships no interpreter,
expression language or WASM runtime; custom logic stays in hooks, webhook
receivers and workflow endpoints, and workflow definitions and plugin
packages refuse unknown members such as
script(ADR 0021). - LDAP and Active Directory federation and synchronization
(PROV-011..PROV-014, PROV-017, PROV-018, FED-015, USR-022; migration
0065;internal/directory;TestLDAPDirectorySignIn,TestDirectorySynchronization,TestDirectoryAuthority,TestDirectoryTenantIsolation): directory connectors at/api/v1/realms/{realm}/directory-connectors(realm workforce connectors, which grant no organization access) and/organizations/{org}/directory-connectors(org.integrations.write; memberships, groups and roles only in that organization, within a ceiling bounded by the creator's). Orderedldaps://or StartTLS endpoints with verified TLS, failover only on transport errors, referrals never followed, egress checks at connection time (newdirectory.*keys); an encrypted, write-only service-account credential. Passwords of directory-sourced accounts are verified by a bind as the user and fail closed (503 upstream_unavailable, never a local password); realm connectors can provision accounts at first sign-in. Full and incremental runs (Active Directory USN bound to the domain controller, OpenLDAPmodifyTimestamp) asdirectory.syncjobs scheduled by the worker taskdirectory.schedule, with dry runs, conflict findings that never merge accounts, adoption only through an organization's verified domain, a deletion threshold with an audited override, and deprovisioning that suspends the account (realm) or removes or suspends only the membership (organization). Read-only, import-only and write-back modes; local edits of externally managed attributes, password changes and reactivation of directory-disabled accounts are refused on every path with409 conflict(externally_managed), andGET /me/attribute-authorityand/users/{user}/attribute-authoritydeclare what is editable. Connector health (.../health), runs and findings; metricsproudauth_directory_*; auditdirectory.*. Kerberos/SPNEGO and password write-back are not offered. Tested only against an in-process LDAP server, not against real directories (directory.md, directory threat model). - Outbound SCIM provisioning (PROV-010, PROV-017;
TestOutboundProvisioning):scim_outbounddirectory connectors push eligible accounts (and, for organization connectors, chosen groups) to a downstream SCIM 2.0 service with a write-only bearer token: outbox events, a change scan and reconciliation runs feed coalesced, level-triggered deliveries (worker taskdirectory.outbound.deliver) withexternalId= the account ID, only verified emails, exponential backoff, dead-lettering and replay, a fair share per connector, and reconciliation findings that never delete resources the connector did not create. Tested only against an in-process SCIM double (outbound-provisioning.md). - New permission
realm.provisioning.read(held by the built-inrealm-auditorrole): read directory and outbound connectors, their runs, findings, health and deliveries, never credentials;realm.provisioning.managemanages them. - Shared Signals transmitter (AUD-016, migration
0058), a declared profile of OpenID Shared Signals Framework 1.0 with CAEPsession-revoked/credential-changeand RISC account and credential events, opt-in per realm (shared_signals.enabled). Receivers are machine clients holdingrealm.ssf_streams.manage; events are queued with their audit evidence, only about users who authorized the receiver unless an administrator widens a stream, and delivered as signed Security Event Tokens by RFC 8935 push (worker taskssf.deliver) or RFC 8936 poll (no long polling). Administrators see every stream and its delivery state (/ssf-streams) (TestSharedSignalsTransmitter). - Shared Signals receiver (ADV-007, migration
0083): transmitters an administrator registers (/ssf-transmitters) push SETs toI/ssf/receivers/{id}/eventswith a push secret; signature, issuer, audience, age and replay are checked, subjects map through a federation connection or verified email, and each event type maps to an explicit action (record only by default; never re-enabling an account or suspending a protected one). Records keep no token or subject identifiers and are deleted after 30 days (TestSharedSignalsReceiver; shared-signals.md, ADR 0022). - Container-registry token bridge (ADV-010, migration
0082):/registry-servicesmaps repository patterns to permissions, andI/registry/tokenimplements the Distribution token protocol, issuing short-lived tokens for the registry's audience with only the repository actions the principal's permissions and credential scopes allow (TestRegistryTokenBridge; container-registry.md). - Historical entitlement evidence (ADV-011):
POST /investigations/entitlements(realm.audit.read, required reason, audited) reconstructs a subject's account state, memberships, groups, role assignments and permissions, live OAuth grants, ABAC policy versions and realm policy revision at a past instant from retained evidence, with completeness and caveats (TestHistoricalEntitlements; investigations.md).
Migration#
The runbook is docs/operations/migration.md
(MIG-001..MIG-017); the endpoint reference is
exit-and-export.md
(TestImportFactorsAndDeletionContinuity,
TestMigrationPlanningAndReconciliation).
- Declared import formats (MIG-002):
POST /user-importstakesformat(defaultproudauth.user-import.v1; any other value is refused with422), andGET /user-import-formats(realm.users.import) lists each accepted format with its source products, schema andsupports/unsupportedlists. - Dry-run plans, conflicts and totals (MIG-003, MIG-004): a record that
repeats a
source_id, email or username of an earlier record of the batch, or whose email or username belongs to an existing account, is reportedconflict(imports never merge accounts by matching identifiers), in dry runs and real runs. Dry-run results carry aplanper record (mapping,email_verified,password_verifier,totp_factor, andmembershipswithorganization_id, role names andprivileged), and every result hastotals(memberships,role_assignments,privileged_memberships,trusted_verified_emails,password_verifiers,totp_factors). - Resumable imports (MIG-011): the checkpoint saved every 50 records now
holds the counts, totals and results, so a retried or resumed job keeps the
results recorded before it; a checkpoint that does not match its position
restarts the job from the first record (records already created then report
already_imported). - Reconciliation report (MIG-012):
GET /user-imports/reconciliation?source_system=...(realm.users.import) reports, for the accounts imported from one source system, mappings by account state, accounts, memberships, privileged role assignments (up to 200 listed), credential usability (password, not yet rehashed, TOTP, passkey, federated, no sign-in method), verified emails, accounts that have signed in and subject continuity, and compares optionalexpected_*counts inmismatchesandreconciled;404when nothing was imported from that source. - TOTP seed import (MIG-007): with
import_totp: true(requiresrealm.users.credentials.manageand an MFA session, re-checked by the worker), records may carrytotp_secret(base32, 80–512 bits; SHA-1, 6 digits and 30 s assumed). Seeds are re-encrypted under the deployment's data key and audited asmfa.totp.imported; a record with a seed fails withoutimport_totp. - Deletion continuity (MIG-017): a
source_idmapped to an account that was deleted since is reportedskipped_deleted, like an email or username released by an account deletion, so an import never recreates a deleted account. - Historical audit import (MIG-016, migration
0038):POST /audit-imports(new privileged permissionrealm.audit.import, MFA session) files up to 500 records of a source system's audit history per request in the new tableimported_audit_events, apart from local evidence: never sealed into the hash chain, always reported withsource: "import"andverified: false, subjects mapped through the user import mappings of the same source system, duplicates bysource_event_idleft unchanged. Each request records one localaudit.history.importedevent (categoryaudit) with abatch_digestover the records' content hashes.GET /imported-audit-events(realm.audit.read) lists them by source system, subject, organization, type, batch and time. Retention is counted from the import (privacy.audit_retention, or the new categoryimportedofprivacy.audit_retention_by_category; legal holds apply), and account deletion clears the source-supplied details of records about the account. - Realm data export (OSS-008, MIG-014):
POST /data-exports(realm.data.export, privileged; recent authentication) queues arealm.exportjob whose NDJSON file (proudauth.realm-export.v1) holds a header listing what is excluded, the configuration document, and the realm's accounts (identifiers, external IDs, linked identities and a credential summary), organizations, memberships, groups, role assignments, authorization policies, projects, service principals, identity providers, webhook subscriptions and hooks, with stable IDs and without credentials, secrets or live tokens. The job result has counts per kind and the file's SHA-256; the permission is re-checked when the job runs and when a download token is issued (TestRealmDataExport; exit-and-export.md). - Legacy hash deadline (CRED-004):
security.password.legacy_hashes_until(UTC date, inclusive) ends the migration window for imported salted SHA-256 verifiers: afterwards they no longer verify (the sign-in fails like a wrong password; auditcredential.password.legacy_retired, reasonLEGACY_HASH_RETIRED) and user imports refuse them.identity-cli doctorprints how many accounts still use one (TestLegacyHashDeadline; migration.md). - First-party migration adapters (MIG-018):
identity-cli migrate-from keycloak|zitadel|supertokensconverts a Keycloak 26.x realm export, a ZITADEL v4.x admin API data export or a SuperTokens core 12.x export (produced read-only from the core database with the query--print-export-sqlprints) intoproudauth.user-import.v1records and the organizations they need, translates source roles and groups only through a reviewed mapping (proudauth.migration-mapping.v1; an absent key grants nothing), writes a capability report (proudauth.migration-report.v1: per entity what converts, converts with loss, is unsupported or ignored, without verifiers, seeds or emails) and runs dry-run imports (default) or, with--apply, creates the organizations (with source-qualified external IDs, so re-runs reuse them) and imports through the ordinary authenticated APIs. Tested versions: Keycloak 26.8.0, ZITADEL v4.16.3, SuperTokens core 12.0.7; other major versions are refused. Two read-only endpoints (realm.users.import):GET /migration-adaptersandPOST /migration-plans(converts up to 20 export documents in memory and checks them against the realm) (TestMigrateFromKeycloak,TestMigrateFromZitadel,TestMigrateFromSuperTokens,internal/migrate; migration-adapters.md). - Imported bcrypt and PBKDF2 verifiers (MIG-006, CRED-004): user imports
accept bcrypt (
$2a$,$2b$,$2y$, cost 10–15) and PBKDF2 ($pbkdf2-sha1|sha256|sha512$i=<n>$<salt>$<dk>, 10,000–2,000,000 iterations, salt 8–64 bytes, key 16–64 bytes), checked against the password as typed and its NFKC form and rehashed to Argon2id at the next successful sign-in. Both are legacy verifiers covered bysecurity.password.legacy_hashes_until, andidentity-cli doctorcounts them (TestImportedForeignVerifiers,TestImportedVerifierRawPassword; exit-and-export.md). - Import record members (MIG-006, AUT-018):
proudauth.user-import.v1records acceptgiven_name,family_name,locale,state(activeorsuspended),required_actions(each needing the same permission as on an existing account, checked at the request and in the job) andadmin_metadata(at mostlimits.metadata_bytes). Dry-run plans show the state and required actions; job totals addsuspended_accountsandpassword_updates_required;user.importedaudit metadata addsstateandrequired_actions(TestMigrateFromImportRecordRules).
Operations#
identity-server,identity-worker,identity-cli(migrate, doctor, realm-create, bootstrap with break-glass option, verify-audit, support-bundle, revocation-log, restore-reconcile, and OAuth-authenticated API commands).Versioned root keys with HKDF purpose keys; signing-key lifecycle with planned and emergency rotation (TEST-036).
Embedded migrations with advisory lock, checksums and refusal of unknown migrations; readiness checks for database, schema, signing keys and clock skew.
Prometheus-format metrics and
/statuson an internal listener; trace IDs in logs and optional sampledspanlog records; support bundle.Revocation log and restore reconciliation (TEST-060).
Restore marker (TEST-060, OPS-014, migration
0087, expand-only):identity-cli restore-beginmarks a restored database (auditedoperations.restore.begun;-statusshows it); while marked,GET /health/readyanswers503 {"status":"unavailable","reason":"restore_pending"}andidentity-workerrefuses to start, untilidentity-cli restore-reconcileclears the marker in its transaction (reportrestore_marker_cleared). Databases never marked are unaffected (TestRestoreProofConsumption,TestRestoreBeginAndReconcileCommands; backup-restore.md).Disaster-recovery drill (DEP-019, OPS-007):
make dr-drill(scripts/dr-drill.sh) runs the promotion runbook and a point-in-time recovery against real PostgreSQL 18 replication with throwaway containers and the binaries of the checkout: a primary with WAL archiving and a synchronous (or, withDRILL_SYNC=off, asynchronous) streaming standby, ProudAuth state created through the API, the primary fenced under write load, the standby promoted,restore-begin,doctorfingerprint comparison,restore-reconcilewith the exported revocation log, and sign-in, token, introspection and revoked, deleted and suspended state verified; then a base backup plus archived WAL restored to a target time between two changes, with the later changes absent and the revocations exported after the target re-applied. It prints the measured RPO (acknowledged transactions lost) and RTO (fencing to the first token). A weekly and on-demand workflow (.github/workflows/dr-drill.yml) runs it (backup-restore.md). Runbook corrections from the drill: clearsynchronous_standby_nameson a promoted standby or a point-in-time restore (it comes withpostgresql.auto.conf, and every commit otherwise waits for a standby that is gone), and verify under maintenance moderestricted, becauseunavailablerefuses sign-in and token requests.Failure-mode tests (TEST-059, docs/plan.md §31.2) with test-only fault-injection seams in the style of
impersonation.FailAudit:storage.DB.SetFaultfails a chosen transaction at its start or just before its commit,keys.Keyring.SetFaultmakes the key provider unreachable for a purpose.TestTransactionFaultsLeaveNoPartialState(refresh rotation, code exchange, password-reset completion, invitation acceptance: every transaction failed in turn, nothing half-applied, the proof stays usable),TestSigningKeyOutageDuringIssuanceandTestDeliveryOutageDuringSensitiveFlows(mail down during password reset, email verification and invitation).Dockerfile, Compose development stack, Kustomize manifests, offline bundle script (
make bundle), CI (tests with PostgreSQL 17, fuzz smoke, secret scan, license policy, govulncheck, staticcheck, image smoke test) and a release workflow with SBOM and build provenance.Migrations
0019–0023are expand-only (new tables, one new index onnotification_jobs, and in0023a backfill of alert claims for expiry notices already sent); none is marked-- proudauth:compat=contract, so binaries without them keep running during a rolling upgrade (upgrades.md).Migrations
0024–0027are expand-only as well (arealmscolumn with a default, the one-rowsystem_statetable and its debug-window columns, and two nullableuserscolumns with a partial index); none is marked-- proudauth:compat=contract. Binaries without them ignore emergency grant controls, maintenance mode, debug windows and lifecycle rules, so finish the rollout before relying on these controls.Migrations
0028–0031are expand-only too: nullable columns onscopes(step_up_acr,max_auth_age_seconds),clients(consent_terms_version),consents(preauthorization_id),organization_domains(proof_digest) andservice_principals(owner_departed_at), the counterorganization_domains.recheck_failureswith default0, the newconsent_preauthorizationstable and two indexes; none is marked-- proudauth:compat=contract. Binaries without them ignore scope step-up requirements (they grant such scopes without stepping up), consent terms versions and pre-authorizations, domain re-verification and owner-departure handling, so finish the rollout before configuring these.Migrations
0032–0037are expand-only as well: new tablesorganization_templates,organization_aliases,audit_legal_holds,audit_prune_log(append-only, guarded by theaudit_prunestrigger function),hook_invocationsand the one-rowaudit_archive_state; nullable columns onorganizations(template_name,template_version) andgroups(rule_recomputed_at,rule_member_count,rule_error);organization_domains.include_subdomainswith defaultfalse; and two indexes. None is marked-- proudauth:compat=contract. Older binaries ignore legal holds and per-category retention when they prune audit events, do not resolve aliases or route subdomains, do not recompute dynamic groups, do not archive audit batches, and send an organization's mail through the deployment relay; they also reject the new keysprivacy.audit_retention_by_categoryandprivacy.audit_archive_diras unknown. Finish the rollout before placing legal holds or configuring these features (upgrades.md).Trace propagation and event correlation (API-012): outbound HTTP calls made while serving a request (pre-action hooks, upstream identity providers, challenge providers) carry the request's W3C
traceparent(notracestateor baggage), and webhook events carrycorrelation_id, theX-Request-Idof the causing request and therequest_idof its audit events; events from background work have none (TestCorrelationPropagation; monitoring.md).Migrations
0038(theimported_audit_eventstable) and0039(audit_archive_state.directory) are expand-only; neither is marked-- proudauth:compat=contract. Older binaries do not serve the import, reconciliation and imported-audit routes, do not clear imported records on account deletion, and archive and prune audit batches the old way, so run the archive and retention on new workers only and import audit history after the rollout (upgrades.md).Audit failure observability (AUD-014): gauges
proudauth_audit_unsealed_eventsandproudauth_audit_unsealed_oldest_seconds(refreshed on every scrape),proudauth_audit_integrity_problems(set by the worker taskaudit.verify_recent) and the counterproudauth_jobs_failed_total{type}(asynchronous jobs that reacheddead_letter, counted by the worker).audit.verify_recentruns hourly and verifies the newest 200 sealed batches (content hashes, digests, links, and signatures where present); problems also raiseproudauth_security_signals_total{signal="audit_failure"}on the worker and logaudit integrity problems(event_codeaudit.integrity_failed).GET /statusadds theaudit_sealingcapability and backlog (TestAuditRetentionCategoriesAndHolds; monitoring.md).Tenant fairness (TEN-018): per-organization budgets
security.rate_limits.login_per_organization(password attempts in transactions that name an organization, perlogin_window, default 10000) and the existingsecurity.rate_limits.organization_per_window(access tokens minted in an organization's context, pertoken_window, default 2000; fails closed), and worker sharesworkers.tenant_batch_share(at most 25 due notifications of one organization, or of a realm for messages without one, and 25 webhook deliveries of one subscription per batch) andworkers.max_jobs_per_organization(at most 2 running jobs per organization);0disables each (TestTenantFairness; configuration.md).Emergency controls (SEC-020, migration
0024):POST /api/v1/realms/{realm}/emergency/controls(realm.emergency) withtarget_type(grant_type,client,identity_provider,organization,service_principal,hookorwebhook),target_id,action(disableorenable) and a requiredreason(at most 500 characters). Grant classes (authorization_code,refresh_token,client_credentials, and the device-code and token-exchange grant URNs) are stored inrealms.disabled_grant_typesand refused realm-wide by the authorization, device-authorization and token endpoints (unauthorized_client); whilerefresh_tokenis disabled no refresh tokens are issued. The operator CLI clientproudauth-cliis exempt so the control can always be reversed. Identity providers, hooks and webhooks are switched without revalidating their configuration or destination.GET .../emergency/controlsshows the realm state, the disabled grant types and up to 200 suspended or disabled targets of each kind. Every audit event a control causes is filed in theemergencycategory, with the operator's reason when the event carries none, and raises theemergency_actionsecurity signal (TestEmergencyControls; incidents.md).Maintenance mode (OPS-015, migration
0025):identity-cli maintenance -mode off|restricted|unavailable -message "..." -retry-after N(auditedoperations.maintenance.changed), observed by every server within 2 seconds.restrictedkeeps sign-in, token issuance, introspection, revocation, logout and emergency controls available, as well asPOSTrequests whose path ends in/revoke-sessions,/suspendor/lock, and refuses other changes;unavailableserves only health, discovery, JWKS, introspection, revocation and logout. Refusals are503with problem codemaintenance,Retry-Afterand the operator's plain-text message (TestMaintenanceAndStatus; incidents.md).Debug logging windows (SEC-015, migration
0026):identity-cli debug-window -for 15m -component worker -reason "..."emits records belowobservability.log_levelfor a bounded time (at most 1 hour in production, 24 hours otherwise), optionally for one logcomponent, with redaction unchanged;-closeends the window. Opening and closing are audited (operations.debug_window.opened,.closed) (TestDebugWindow; support-bundle.md).Configuration diff (DEP-020):
identity-cli config-diff -from <current> -to <proposed> [-json]validates the proposed file and lists every changed key with redacted old and new values, its effect (alwaysrestart: there is no file watcher) and whether it moves a security boundary (TestDiffClassifiesAndRedacts; configuration.md).Graceful shutdown (OPS-012): on
SIGINT/SIGTERMthe server first answers/health/readywith503 {"status":"draining"}forserver.drain_delay(default 5 s), then stops accepting connections and drains for up toserver.shutdown_deadline. The worker stops claiming work, lets the task in progress finish for up toworkers.drain_timeout(default 20 s) and then returns the jobs, notifications and webhook deliveries it still holds to the queue. The Kubernetes manifests' termination grace periods (30 s for the server, 60 s for the worker) cover the defaults (TestWorkerShutdownReleasesLeases; installation.md).Migration safety gates (OPS-008):
identity-cli migrate -plan, contract (destructive) migrations only with-backup-reference, and-- proudauth:preconditionchecks; a mixed-version window in which older binaries keep running against newer expand-only migrations (TEST-061,TestMixedVersionWindow,TestMigrationSafetyGates).Database-enforced realm boundaries: a trigger rejects role assignments and assignment requests whose subject or scope lives in another realm, and realm/organization foreign keys on webhook subscriptions, webhook deliveries and jobs apply to new and changed rows (added
NOT VALID;identity-cli doctorreports older orphan rows) (migration0016, DB-001,TestAssignmentRealmBoundary).Secret references
cred:NAME(systemd credentials) andvault:<mount>/data/<path>#<field>(HashiCorp Vault KV v2), and a registry for operator-built resolvers (DEP-006).identity-clicommandsclients,roles,assign-role,grant-scopes,sessions,invitations,support-grants,permissionsandpassword-calibrate.Generic cursor pagination for list endpoints: opaque cursors MAC-bound to the realm, route and filters (API-004,
TestListPagination).Security-signal metrics
proudauth_security_signals_total{signal}derived from audit evidence (OPS-004,TestSecuritySignalsMetrics), plusproudauth_signing_failures_total{reason},proudauth_worker_active_leasesandproudauth_hook_calls_total; gauges are refreshed at scrape time; the worker can expose its metrics onworkers.metrics_listen; every log record carriescomponentandevent_code.Release workflow publishes the container image by digest with SBOMs, build-provenance and SBOM attestations and a keyless cosign signature; Dependabot for Go modules, npm, GitHub Actions and Docker; CI runs the Node.js SDK tests.
KMS envelope (DEP-016, implemented as unwrap-at-start): root, pepper and signing-wrap keys can be stored wrapped by any KMS or HSM with a command-line client and referenced as
kms:<file>;keys.kms.commandruns without a shell at each process start, receives the wrapped key on standard input and prints the key. The keys are then held in process memory; signing inside an HSM is not supported./statusreportskey_provider(softwareorkms_unwrapped) (TestKMSUnwrap; keys.md).Trace export (OPS-003):
observability.otlp_endpointexports sampled server spans (method and route, status) and client spans of outbound calls (method, host, status) as OTLP/HTTP JSON, batched and dropped rather than delayed; thetracing.Tracerinterface accepts other exporters (TestTraceExport,TestOTLPExporter; monitoring.md).Custom domains (DEP-014, migration
0041):/custom-domainsregisters realm host names proven by a DNS TXT record (realm.settings.write, MFA session), andserver.host_policy: strictserves only the public origin and verified custom domains (each for its own realm's paths), answering421 misdirected_requestotherwise. Issuers and subjects never change; cookies are per host (TestCustomDomains; installation.md).Database role separation (OPS-013):
storage.migration_database_urlruns migrations as the schema owner andstorage.runtime_rolereceives data privileges only after each run (noDELETEon the audit tables);identity-cli doctorwarns when the runtime role is a superuser, owns the schema, inherits the owner's privileges, can create roles or can create objects inpublic(TestRuntimeRoleSeparation; installation.md).identity-cli revocation-lognow records the operator audit eventoperations.revocation_log.exported(since,records).Data residency (PRV-003):
privacy.residency.allowed_destinationskeeps outbound connectors (webhooks, hooks, back-channel logout, SMS gateway, organization mail servers, upstream providers, challenge providers) on listed hosts or domains, checked by the egress policy;privacy.residency.regionanddestinations_restrictedappear in/status.privacy.backup_retentiondates the backup stage of the deletion report (TestResidency; data-inventory.md).Benchmark suite (NFR-003..NFR-008):
cmd/identity-benchseeds a dedicated realm through the public API and runs workload, adversarial, consistency-stress and soak scenarios with JSON results (latency percentiles and histogram, outcomes, environment);make benchreproduces the default run. Laptop reference numbers, software passkey/SCIM workloads and SMTP worker scenarios are documented; physical authenticator latency, webhook capacity and multi-node consistency beyond refresh races remain outside the measured baseline (TestBenchSuiteSmoke; benchmarks.md).TestTimeAndUnicodeInvariants(NFR-010) checks that token lifetimes are absolute (iat/expin Unix seconds) with the host time zone set to New York shortly before a daylight-saving change, that case variants of an email address and case and full-width variants of a username resolve to one account, and thatİlower-cases toiin usernames without locale rules.Supply chain (GOV-004): the release workflow runs the whole CI workflow (
workflow_call) before it publishes; pull requests get adcojob that fails when a commit lacksSigned-off-by; the Node.js job runsnpm audit --audit-level=high;govulncheck(pinnedv1.8.0, also inmake vuln) additionally scans unstripped rebuilds of the shipped binaries in-mode=binary; CI and release use the latest Go 1.27 patch release (go-version: "1.27.x",check-latest: true) and the image builds withgolang:1.27.2-trixie, so standard-library security fixes no longer wait for ago.modchange;.github/CODEOWNERSnow also requires security reviewers forinternal/machine,internal/platform/secret,internal/provisioning,internal/provision,internal/audit,internal/configuration,internal/restore,api/, all ofsdk/,.github/,go.mod/go.sum, the secret and license scripts and the security documents (CONTRIBUTING.md);.gitignorecovers the secret-bearing files the CLI and benchmark tool write (seed files, configuration exports,--secrets-outfiles, support bundles, revocation logs).Operational retention (PRV-004, OPS-009): ended grants are deleted
privacy.session_retentionafter they ended, with their refresh families and tokens, codes and opaque tokens, and then ended client sessions and sessions; the newprivacy.history_retention(default4320h, at least168h) deletes revoked consents, ended invitations, finished notification jobs and delivered or dead-lettered webhook and back-channel logout deliveries (TestOperationalRetention; data-inventory.md).Configurable safety limits (OSS-007):
limits.invitation_resends,limits.shared_invitation_max_uses,limits.shared_invitation_max_ttl,limits.client_secrets_maxandlimits.metadata_bytes(organization metadata, user profile and admin metadata);limits.max_batch_sizealso bounds bulk invitations (TestConfigurableLimits; configuration.md).Queue age metrics (OPS-002):
proudauth_notification_oldest_due_seconds,proudauth_webhook_oldest_due_secondsandproudauth_jobs_oldest_queued_seconds, refreshed at every scrape of the server's or the worker's metrics listener (monitoring.md).Worker spans (OPS-003):
identity-workerexports sampled task runs (service.nameproudauth-worker, spanworker <task>, attributesproudauth.worker.taskandproudauth.worker.items) with their outbound calls as child spans (monitoring.md).Cleanup indexes (DB-011, migration
0045): expiry and cleanup queries (codes, opaque tokens, DPoP JTIs, PAR, device codes, idempotency records, dispatched outbox events, ended sessions, the SMS budget, the notification payload purge) use indexes instead of scanning tables (capacity.md).Startup migrations (
identity-server -migrate,migrate_on_start) are audited asoperations.migrations.applied(actorstartup:<hostname>, metadatamigrationsandtrigger) (OPS-013; installation.md).Client configuration as code (APP-019): configuration exports now include each client's protocol options (front-channel logout, response signing and encryption,
tls_client_auth_*and certificate-bound tokens, display URIs), and the configuration document schema lists them withtls_client_auth,self_signed_tls_client_authandsecurity_profile: fapi2. Drift detection also comparesallowed_origins,backchannel_logout_uri,allow_offline_access,allow_remember_me,token_endpoint_auth_method,security_profileand the protocol options. Clients backing SAML applications are left out of documents (exit-and-export.md).
SDKs, contracts and examples#
Go SDKs:
sdk/go/verifier(offline verification, strict-online introspection,SupportGuard),sdk/go/oidcclient(BFF client),sdk/go/admin.Node.js/TypeScript SDK
sdk/node(unpublished; now 0.2.0) with the same four areas.Dart SDK
sdk/dart(proudauth_sdk0.2.0, unpublished, Dart 3.4+, no Flutter dependency): offline verification (Verifier,MultiIssuerVerifier), strict-online introspection withSupportGuard, bounded-state verification, the OIDC/BFF client with single-flight refresh, and the administration client (Secret,onDeprecated, idempotency keys,If-Match). RS256, PS256, ES256 and EdDSA verification is implemented in the package onBigInt(no native crypto dependency) and checked against vectors and tokens signed by Go. Adapters: the framework-neutralauthorizeBearerandpackage:proudauth_sdk/io.dartfordart:ioservers.make test-dart, CI jobdart-sdk.PHP SDK
sdk/php(proudauth/sdk0.2.0, unpublished, PHP 8.2+ withcurl,json,opensslandsodium; no Composer dependencies): the same areas, with a key-setStore(MemoryStore,FileStore) so PHP-FPM workers share the JWKS, refresh serialization across workers (FileRefreshLock),RequestAuthenticatorfor plain PHP (a PSR-15 example in its README) and a cURL transport that never follows redirects. Checked against signature vectors signed by Go.make test-php, CI jobphp-sdk(PHP 8.2 and 8.5).Both new SDKs run the shared access-token and support-operation fixtures and send
ProudAuth-SDK: dart/0.2.0/php/0.2.0(sdk-compatibility.md).Bounded-state verification mode in both SDKs (
sdk/go/verifierBoundedState/BoundedVerifier,sdk/nodebounded-state): local JWT checks plus revocations replayed from the session-event stream, failing closed when the state is older than the configured bound (SDK-002,TestBoundedStateVerification).OpenAPI document, configuration/import JSON Schemas and event schemas under
api/, with contract tests (tests/contract).Shared verification fixtures (SDK-009):
sdk/fixtures/access-tokens/cases.jsonholds 18 accepted and rejected access tokens with a test-only public JWKS; both SDKs run every case (sdk/go/verifier/fixtures_test.go,sdk/node/test/fixtures.test.ts), and independent verifiers can use them (sdk/fixtures/README.md).Fuzz targets (QA-003):
FuzzPolicyDefinition(internal/authorization),FuzzParseTraceparent(internal/platform/reqctx),FuzzAllowed(internal/egress: the public-only policy never allows private, loopback, link-local, unspecified or multicast addresses) andFuzzClaimMappings(internal/application), run by the CI fuzz smoke andmake fuzz.correlation_idin the event envelope (api/events/envelope.v1.json, optional, API-012).Examples: backend-for-frontend, protected orders API (strict-online), protected billing API (offline), machine client.
SDKs 0.2.0 (SDK-007, SDK-008, SDK-010..SDK-012; see Changed for the breaking part): the administration clients send
ProudAuth-SDKand a versionedUser-Agent, reportDeprecation/Sunsetheaders (Response.Deprecation/SunsetandClient.OnDeprecatedin Go,deprecation/sunsetandonDeprecatedin Node.js) and wrap one-time secrets (admin.Secret,Secret). New adapters: Node.jsfastifyHookand Goverifier.AuthenticateMetadatafor gRPC metadata, WebSocket upgrade and message headers. Multi-issuer verification (INT-009):verifier.NewMultiIssuer/MultiIssuerVerifiermap explicitly registered issuers to tenants and refuse all others.identity-cli config-export,config-plan(with adriftlist of existing objects that differ from the document) andconfig-apply(confirmation,--secrets-out) (sdk-compatibility.md).Shared support-operation rule (IMP-011, IMP-013): both SDKs export the write-verb list and a classifier (Go
verifier.WriteSegmentList()andIsWriteOperation, Node.jsWRITE_SEGMENTSandisWriteOperation, next toverifier.ProtectedOperationList()/PROTECTED_OPERATIONS), and both SDKs and the server's decision API run the cases insdk/fixtures/support-operations/cases.json(TestSupportClassificationMatchesSDKTable).Go
verifier.ErrNotStarted:BoundedState.Syncbefore a successfulStartreturns it instead of replaying from an empty position;Runstarts the stream itself (resource-servers.md).Configuration import reconcile (SDK-012):
?mode=reconcileonPOST /configuration/validateand/imports(identity-cli config-planandconfig-apply -reconcile) updates existing clients, resources and role permissions that differ from the document; nothing is deleted (TestConfigReconcile; exit-and-export.md).
Documentation and governance#
- README, SECURITY, CONTRIBUTING (DCO, review rules), CODE_OF_CONDUCT, CODEOWNERS, issue and pull-request templates.
- Architecture decision records 0001–0020, operations runbooks, protocol profiles, integration guides and threat models.
- API compatibility policy: versioned surfaces, additive changes, deprecation window and security exceptions (docs/protocols/compatibility.md, API-013).
- Migration runbook (docs/operations/migration.md, MIG-001..MIG-017).
- Integration patterns for HTTP, GraphQL, gRPC, WebSockets, long-lived streams, background jobs, message consumers, download grants, multi-issuer applications and contract validation (docs/integrations/patterns.md, INT-001..INT-010); SDK compatibility policy (sdk-compatibility.md, SDK-010); benchmark guide (benchmarks.md); runbooks for root-key and KMS compromise, connector compromise, abuse spikes, database loss, organization owner recovery and privacy deletion errors (incidents.md, OPS-016); conformance claims and tested versions (interoperability.md, QA-004, QA-006); security regression policy (SECURITY.md, QA-008); partitioning policy (capacity.md, DB-012); accessibility metadata of the transaction API (headless-authentication.md, NFR-009).
- SAML, directory and outbound provisioning guides (saml.md, directory.md, outbound-provisioning.md), their threat models (saml, directory), incident runbooks for SAML key or counterparty compromise and directory connector compromise, and a dated update of ADR 0018 recording what is now implemented and what stays unsupported (including why Kerberos/SPNEGO is not offered).
- Isolation tiers: organization-, realm- and deployment-per-customer patterns with issuer, key, migration, backup, residency and cost tradeoffs (isolation-tiers.md, TEN-019).
- Disaster-region recovery profile: one writing region with a warm standby, a promotion runbook with restore reconciliation, and what is not offered (active-active writes, globally immediate revocation) (backup-restore.md, ADR 0019, DEP-019).
- Capabilities deliberately not offered, with the reasons and what an implementation would need: push approval (MFA-014), CIBA (ADV-005), UMA (ADV-006), verifiable credential issuance and presentation (ADV-008, ADV-009) and JAR; declared protocol profiles (compatibility.md).
- Threat model for cross-system trust surfaces (session iframe, shared signals, registry tokens, passkey trust, federation transfer, partner access, flow revisions, investigations) (cross-system-trust.md); ADR 0022 on security-event exchange.
Changed#
The owner excludes independent security review from the 2026-10-13 delivery's completion gate (scope exception). The project remains unreviewed; all other implementation, verification and release requirements remain in scope.
User/exchanged JWTs require a live grant link online (SES-023): newly issued JWTs carry the reserved
grant_id; exchanges preserve the originating link. Earlier development JWTs without it are refused by online consumers. Obtain fresh access tokens through serialized refresh or reauthentication, coordinate issuer upgrades and rename older mappings with this claim name; see upgrades.md. Offline verifiers retain their documented expiry-based exposure.identity-cli restore-reconcileends every outstanding one-time proof (TEST-060): reset, verification, sign-in and account-setup codes and links, authorization and device codes, pushed requests, pending sign-in transactions, passkey ceremonies, federation and SAML request states, support handoff codes and job download tokens, reported asproofs_invalidated. This also applies when it is used on a live deployment for-invalidate-all-sessions(incidents.md): users mid-sign-in start again and account-setup links must be resent.Request limits have safe ranges (NFR-001):
limits.max_body_bytes262144–67108864,max_header_bytes8192–1048576,max_token_bytes4096–65536,max_json_depth32–256,max_page_size1–1000,max_group_depth1–32,max_role_expansion8–1024andmax_inflight_requests1–100000(0still means 2000). A configuration outside them no longer starts; before,max_body_bytes: 0refused every request body and other zero values fell back silently (TestLimitRanges; configuration.md).identity-cli support-bundlerequires-out: diagnostic files are written only where the operator says, never by default into the working directory (DEP-004,TestSupportBundleNeedsOut). Scripts that relied on the defaultsupport-bundle.jsonmust pass-out support-bundle.json.The release workflow builds binaries with the Dockerfile's exact Go toolchain and
-buildid=, scans every shipped platform with govulncheck, and puts the container image into the offline bundle (TestReleaseToolchainPinned); the license check covers every module the build and tests use and fails when one is missing instead of skipping it; CI renders the Kubernetes manifests; the worker has liveness and startup probes onworkers.metrics_listen(security-checks.md).Go module path is now
github.com/proudauth(wasgithub.com/proudauth/proudauth): update imports ofsdk/go/verifier,sdk/go/oidcclientandsdk/go/admin(for examplegithub.com/proudauth/sdk/go/verifier).Base URL of ProudAuth identifiers is now
https://auth.proudbase.com/(washttps://proudauth.dev/): thetypeof every problem response ishttps://auth.proudbase.com/problems/<code>, and the$idof every JSON Schema underapi/starts withhttps://auth.proudbase.com/schemas/. Clients that compare problemtypeURIs instead ofcodemust accept the new prefix.Configuration keys that previously had no effect now do:
security.password.history_countis a deployment floor under the realm's password history,workers.concurrencybounds parallel asynchronous jobs per worker,workers.max_attemptssets the job attempt limit, and realm policylogin.remember_devicesenables remembered devices (configuration.md).Production configuration now also rejects literal
keys.pepper_keysandkeys.signing_wrap_keys, anddelivery.allow_private_webhooks(usedelivery.egress_allowlist); upstream identity providers on private networks use the newfederation.egress_allowlist.Enabling SMS sign-in requires the realm policy acknowledgement
login.sms_risk_accepted(PWDLESS-005).sessions.client_ipstores a /24 or /48 network unlessprivacy.store_full_ipis set.Environment overrides fail closed (DEP-020): a malformed boolean or an integer outside its range in a
PROUDAUTH_*variable is now a startup error (environment: ...) instead of being read asfalseor0(TestEnvParseErrorsFailClosed; configuration.md).Production configuration rejects
observability.log_level: debug(SEC-015); use a time-limited debug window instead.GET /statuson the internal listener (and onworkers.metrics_listen) requiresAuthorization: Bearer <server.metrics_token>when a metrics token is configured, and its JSON changed (OPS-015):databaseandoutbox_oldest_pending_secondsmoved tocapabilities.databaseandbacklog_oldest_seconds.outbox, and the response addscapabilities(database,schema,signing,outbox,email_delivery,webhooks,jobs),backlog_oldest_seconds,maintenanceand an overallstatus(ok,degraded,maintenance,unavailable). Update monitors that read the old members (monitoring.md).Expired unverified registrations are removed by the new
lifecycle.usersworker task instead ofretention.cleanup.GET /me/consentsitems now carryid,client(internal reference),client_id,client_name,scopes,resource_ids,terms_version,mode(userorpreauthorized),preauthorization_id,granted_atandrevoked_at, newest first (at most 500); revoked consents are listed only with?history=true(APP-013,TestConsentManagement).Configuration as code (APP-019, DEP-020, OSS-008): the
config.exportjob now writes the importableproudauth.realm-config.v1document; an import mergespolicyover the current realm policy instead of replacing it, acceptsexpected_policy_revisionto refuse a stale reviewed document, names service principals by name, accepts grants withsubject_types, rejects environment-bound identifiers in the plan, and is applied in one transaction (all or nothing).POST /configuration/validatenow needsrealm.configuration.import(TestExportsImportsAndJobs; exit-and-export.md,api/schemas/config-document.schema.json).Removed
security.rate_limits.token_per_organization. It duplicatedsecurity.rate_limits.organization_per_window, which already bounds the access tokens minted in an organization's context (every grant type, including token exchange; TEN-018). Unknown keys are startup errors, so delete it from configuration files before upgrading (configuration.md).Audit retention (AUD-009) now removes individual expired events from sealed batches instead of whole batches, so events kept longer (a longer category retention, category
emergency, or a legal hold) stay in their batch. The first pass over a batch is recorded inaudit_prunes, as before, and every later pass in the new append-onlyaudit_prune_log; the chain still verifies (TestAuditRetentionCategoriesAndHolds; incidents.md).GET /statusreports a new capability and backlog,audit_sealing(age of the oldest unsealed audit event;degradedafter 300 s), which also turns the overallstatustodegraded(AUD-014; monitoring.md).A client's
environmentcan no longer change (APP-015):PUT /clients/{client}with a different value is refused with422, and clients in a project take the project's environment by default and must match it. Register a new client for another environment (TestEnvironmentSeparation).Webhook subscriptions to
"*"now also receivesecurity.alertevents (AUD-012), and the worker counts them inproudauth_security_events_total{type="security.alert"}and logs each as asecurity eventwarning, like the othersecurity.*events.PATCH /organizations/{org}requiresIf-Matchwhen the body containspolicy(API-010): without the header the request fails with428 precondition_required, and with a stale version with412 precondition_failed. Read the organization first (GETreturns itsETag, the organization's version, which every change to the organization bumps) and send it back. Clients that patch organization policy withoutIf-Matchmust be updated; other members can still be patched without it, like the routes that already required it (PUTof clients, resources,/settings, identity providers and hooks) (compatibility.md).Claim mappings release attribute values only with the
profilescope (APP-011):user.*andprofile.*mappings inid_token_claimsandaccess_token_claimsnow yield their value only when the grant includesprofile(ID tokens: the requested scopes; access tokens: the token's or its grant's scopes), and otherwise theirdefaultor no claim; static values are unchanged. A realm profile schema with an attribute that is bothclaimableanduser_editableis refused with422(fieldprofile.attributes[<i>].claimable), and such attributes stored earlier are no longer released. Upgrade note: a realm whose policy has such an attribute must clear one of the two flags before its next policy update (PUT /settingsor configuration import), or the update is refused (oauth-oidc-profile.md).Token exchange without
scope(AUT-010): the default ceiling now leaves out scopes whose step-up policy the subject token'sacrorauth_timecannot meet and issues the rest; the request fails withinvalid_scopeonly when no scope remains. A step-up scope named inscopestill fails withinvalid_scope(TestScopeStepUp; oauth-oidc-profile.md).Privileged role assignments (AUD-012, MIG-003, MIG-012):
role.assignedmetadata.privileged, theprivileged_grantsignal andsecurity.alert, the import plan'sprivilegedflag and the reconciliation report now count a role as privileged when it is an owner role, a realm role, or holds a permission the catalog marksprivileged(directly or through a role it composes). The built-in organizationmemberandadminroles no longer raiseprivileged_grant, which removes an alert for every new member (monitoring.md).Minimal webhook payloads keep the non-personal members
signal,event_type,actor_type,subject_type,client_ref,new_kid,previous_kidandprevious_key_compromisedbesides*_id,state,reason,authorization_revisionandmode, sosecurity.alert,client.state_changedandsigning_key.rotatedare complete in the default form;user.created'screated_viais still standard-only (api/events/README.md).privacy.audit_retention_by_categoryaccepts only known category names (account,application,audit,authorization,configuration,data_access,imported,operations,privacy,security,support,token); any other name now stops startup instead of being ignored (TestAuditCategoriesKnown; configuration.md).With
privacy.audit_archive_dirset, audit retention prunes only batches the archive already holds, so nothing is pruned before the first archive run and a stopped archive holds retention back (incidents.md).Organization template roles can no longer carry
support.*permissions (onlyrealm.*was refused before).User imports report an email or username that already belongs to an account as
conflict(in dry runs too) instead of anerrorat creation, andGET /jobs/{id}showsformatandimport_totpin auser.importjob'sinput.Migrations
0040_flow_challenges.sql(wider check constraints for the challenge purposeflow_challenge, the hook kindauthentication.riskand adapter names) and0041_custom_domains.sql(new tablerealm_custom_domains) are expand-only. Older binaries in a mixed-version window ignore the features they back and reject the new configuration keys; add rules, adapters, custom domains and keys only after the rollout (upgrades.md).SDK 0.2.0 (breaking): one-time secrets are wrapped. The Go administration client returns
admin.Secretinstead ofstringfor secret members (client_secret,api_key,setup_token,invitation_secret,access_token,handoff_code,download_token, …) in results decoded intomap[string]anyorany; the Node.js client wraps them inSecretin all response data. Code that reads them as strings must callReveal()/reveal(); printing, logging and JSON-encoding show[REDACTED](sdk-compatibility.md).Required actions need the authority of each action (AUT-018): adding or removing
update_passwordorconfigure_totpneedsrealm.users.credentials.manage, andverify_emailneedsrealm.users.write, inPOST /users/{user}/required-actionsand bulk operations (besidesrealm.users.required_actions; checked at the request and again by the worker) and in therequired_actionsofPOST /users(besidesrealm.users.write); others get403(TestRequiredActionPermissionscovers the first two).security.decision_cache_ttlaccepts0to10mandsecurity.dynamic_group_max_staleness0or10mto168h; other values, including negative ones, now stop startup. The worker taskgroups.dynamicruns every 5 minutes, recomputes the 100 least recently computed groups and then catches up in batches of up to 200 while groups are older than half of the staleness bound (at most 4900 groups per run).Removing a dynamic group's rule keeps its members as ordinary members (source
admin) instead of leaving rule-derived memberships that grant nothing, bumps their membership revisions and recordsgroup.dynamic_rule.removed(members_kept); re-adding an existing member is a no-op without an audit event.User imports refuse a
source_systemthat does not match^[a-z0-9][a-z0-9._-]{0,62}$, like audit imports, external IDs and the reconciliation report already did.proudauth_delivery_totalalso counts final webhook (delivered,dead_letter) and notification (accepted,failed,expired) outcomes, besides back-channel logout (monitoring.md).RP-initiated logout responses carry
upstream_logout; organization-scoped webhook subscriptions are disabled when their organization's deletion starts and no longer receive itsorganization.deletedevent.PATCH /organizations/{org}keepslocale,time_zoneandexternal_idwhen the body omits them (""clears) instead of clearing them (TestOrganizationPartialUpdate).Recovery-restricted accounts and sessions (MFA-010, USR-003): a sign-in whose second factor is a recovery code (and no TOTP or passkey) now also moves an
activeaccount to the staterecovery_restricted(user.recovery_restricted, reasonRECOVERY_CODE_USED); a later TOTP or passkey sign-in, or enrolling a new TOTP or passkey from the restricted session, returns it toactive(user.reactivated, reasonFACTOR_ASSURANCE_RESTORED) and lifts the session's restriction. The restricted session may only read and callPOST /me/totp/enroll,/me/totp/activate,/me/webauthn/registration-options,/me/webauthn/registration-complete,DELETE /me/sessions/{id},POST /me/sessions/revoke-allandDELETE /me/remembered-devices[/{id}]; every other write is403with the new problem coderecovery_restricted(TestMultiFactorAndProofs; account-recovery.md). Clients that react to account states must acceptrecovery_restricted.Adding a factor needs an existing one (MFA-006): when the account already has an active TOTP or passkey, the four self-service enrollment routes need a session that completed MFA (a recovery-code session counts); otherwise
403 step_up_requiredwith the obligation{"type": "reauthenticate", "required_profile": "mfa"}(TestFactorEnrollmentNeedsExistingFactor,TestPasskeys).Profile attribute visibility (USR-002, USR-007):
GET /me,PATCH /meand the self-service data export include only attributes the realm schema defines withvisibility: self; administrator views includeselfandadminattributes and undefined ones, neverinternal. AdminPATCH /users/{user}type-checksprofilevalues against the schema (unknown attribute422), limitsprofileandadmin_metadatato 16 KiB each and bounds name fields like self-service; administrators may set attributes of anysource(data-inventory.md) (TestProfileAttributeVisibility).Account deletion needs ownership transfer (USR-020):
DELETE /users/{user}andPOST /me/deletion-requestanswer409 last_ownerwith the extensionowned(projects,clients,service_principals,resources,identity_providers: IDs) while the account owns live objects; move them first (see Added). Offboarding scripts must transfer ownership before deleting.Organization switch follows the organization's sign-in rules (TEN-005, TEN-012,
TestOrganizationSwitch):POST /me/organization-grantsrefuses a session without a federated factor frompolicy.sso_provider_idwhensso_required(403 sso_required), a session without an allowed method (403 step_up_required, obligation withallowed_login_methods), and a session past the organization'ssession_absolute_seconds(403 step_up_required); the new grant expires no later thanauth_timeplus that lifetime (ADR 0012).Groups (TEN-008, TEN-013): adding a group member, or moving a group under another parent (
PATCHwithparent_id), needs the authority to assign every role of the group (or new parent) and its ancestors (403), and a move counts the moved subtree's height againstlimits.max_group_depth(422).group.updatedrecordsparent_idandprevious_parent_id, and every group update bumps the membership revision of all members of the subtree (TestDelegationCeilings; ADR 0006).Domain conflicts (TEN-009,
TestDomainReverification): verifying a domain another organization of the realm holds verified sets the claim toconflict, recordsorganization.domain.conflict(categorysecurity, outcomedenied) and answers409.Required actions (AUT-018):
POST /userswithrequired_actionsneeds the permission of each action (realm.users.credentials.manageforupdate_passwordandconfigure_totp,realm.users.writeforverify_email), likePOST /users/{user}/required-actions; it does not needrealm.users.required_actions.POST /users/{user}/required-actionsis now a compare-and-set: a concurrent change makes it fail with409 conflict("the required actions changed concurrently; retry") (TestRequiredActionEditRace). ThePOST /userscheck has no regression test yet.Conditional required-action edits (AUT-018, API-010, SEC-008,
TestRequiredActionEditRace): the compare-and-set above only covered the request's own read, so two administrators who each read the list, changed a different action and wrote it back could both succeed, the later one silently undoing the earlier.POST /users/{user}/required-actionsnow acceptsIf-Matchwith the ETag ofGET /users/{user}and answers412when the account changed since that read; a successful edit returns the newETag. Adding or removing an action through bulkadd_required_action, the assisted-recovery factor reset or passkey enrollment now moves the account version (and so the ETag ofGET /users/{user}andGET /me), so such a change also makes an earlierIf-Matchstale. Actions that sign-in completes inline still do not move it (threat model).Redirect targets on the identity provider's own origin (APP-004,
TestRedirectRegistrationRules):redirect_urisandpost_logout_redirect_uriswhose host is that ofserver.public_originare refused at registration and update (422, "must not point at the identity provider itself"). Existing clients with such URIs keep working until their next update (oauth-oidc-profile.md).Support writes through the decision API (IMP-011, IMP-013): an action is a write when any segment is a write verb, whatever its suffix, and an
allowlisted_writesupport token may perform a write only when the resource lists the action inimpersonation_policy.write_actions(SUPPORT_ACCESS_WRITE_NOT_ALLOWLISTEDotherwise). Resources that relied on support writes through/authorization/checkmust list them (resource-servers.md).security.totp_skew_stepsaccepts0–2andsecurity.recovery_code_count5–20(MFA-002); other values stop startup (configuration.md).Discovery's
grant_types_supportedleaves out grant types the realm disabled with an emergency control (OIDC-015,TestEmergencyControls).Transient sessions never yield an offline grant or a refresh token, even when
offline_accesswas requested and consented, also when a later sign-in reuses the session through SSO; the token endpoint mints no refresh token for a transient session's grants (SES-020,TestOfflineConsentAndTransientSessions; see Security fixes).make benchrefuses to start withoutBENCH_URLandPROUDAUTH_BENCH_ADMIN_TOKEN, seeds with the current time unlessBENCH_SEEDis set, and therefresh-racescenario reportsinconclusive_rate_limited,inconclusive_http_5xxandinconclusive_transport_errorinstead of a violation when no attempt reached a decision (benchmarks.md).identity-cliwrites stored credentials,config-exportandconfig-apply --secrets-outfiles and support bundles with mode0600through a temporary file and a rename (revocation-log --outappends to a0600file instead).SDK middleware: Go
Middlewarefailures carryCache-Control: no-storeand scope or organization errors answer403withBearer error="insufficient_scope"; the Node.js adapters send that header for organization mismatches too. The GoIntrospectorrefuses support tokens (ErrTokenKind) and a configuration that accepts them (ErrSupportKind); support tokens go throughSupportGuard.OnDeprecated/onDeprecatedreceive the path without its query (sdk-compatibility.md).Go SDK:
verifier.ProtectedOperations,verifier.WriteSegmentsandverifier.DefaultOnlineKindsare no longer exported variables, so code importing the package cannot weaken the support-access protections or the introspector's accepted token kinds at run time. Read them withverifier.ProtectedOperationList(),verifier.WriteSegmentList()andverifier.DefaultOnlineKindList(), which return copies; add protected operations of your own withSupportGuard.ProtectedOperationsas before. Migration: replace reads of the variables with the functions; code that modified them no longer compiles (by design).Bounded-state verification (Go
BoundedState, Node.jsBoundedState):Start/start()no longer clears the revocations already replayed (the stream has no snapshot that could supersede them), the state counts as fresh forMaxStalenessonly once a sync has reached the end of the stream, and a410(cursor older than the retained history) rewinds to the beginning of the retained history and reports GoErrCursorExpired/ Node.jsCursorExpiredError(codeunavailable);Run/run()then replays that history at once (once in a row) and keeps following the stream. A revocation time only moves forward, so a replay never weakens the state.user.recovery_restrictedis no longer treated as a subject revocation (the server keeps those sessions valid), and Node.jsrun(signal)aborts an in-flight long poll and a backoff wait at once (sync(waitSeconds, signal)passes the signal tofetch) (resource-servers.md).Configuration import authority (ARC-008,
TestConfigImportAuthority): applying a document needs, besidesrealm.configuration.import, the write permission of every change in the plan (realm policyrealm.settings.write; clients and grantsrealm.clients.write; resourcesrealm.resources.write; roles and permissionsrealm.roles.write;403otherwise). A document that changes the realm policy must carryexpected_policy_revision, likeIf-MatchonPUT /settings(422, "set expected_policy_revision (the realm is at N)")./validateand dry runs check neither. Migration: give importing service principals the write permissions and addexpected_policy_revision(thepolicy_revisionofGET /settings) to documents that change the policy (exit-and-export.md).Configuration import and export of client resource grants (SDK-012, TOK-016): entries accept
exchange_policy(accept_from_audiences,max_scopes), and the export includes it when set. An existing grant keeps thesubject_typesandexchange_policythe document omits instead of being reset to["user"]and no policy; a grant is unchanged when its scopes, and its subject types and exchange policy where given, match.The configuration export includes every active client and resource (paged, no 1000 cap), and the configuration document schema describes the policy members
auditandauthentication_flow. The plan reports drift of a resource'simpersonation_enabledandimpersonation_policy.Operator commands (
migrate -status,migrate -plan,doctor,realm-create, ...) never apply migrations, whateverstorage.migrate_on_startsays; onlyidentity-cli migrateand server or worker startup do.identity-cli migrate -planis read-only and works with the runtime role (OPS-013; upgrades.md).Database roles (OPS-013):
storage.runtime_roleis validated at startup; the grants after each migration run also set default privileges for later tables and sequences, revokeCREATEon schemapublicandDELETEon the audit tables from the runtime role (it prunes throughproudauth_audit_prune, migration0044), andidentity-cli doctoralso warns when the role inherits the owner's privileges or can create objects inpublic(installation.md).Custom domains (DEP-014, migration
0043): only a verified domain reserves its host across realms; a pending one is unique within its realm (409, "already registered in this realm"), and verifying a host another realm verified first answers409("the host is already in use").passkeys_usableuses the realm's effective RP ID (webauthn.rp_id, else the login UI host, else the issuer host), and without configuredwebauthn.originsthe verified custom hosts under the RP ID are accepted origins. The host cache keeps its previous list when the database cannot be read (installation.md).keys.kms.timeout(at most1m, default10s) now limits each unwrap; secret references are bounded at 5 minutes overall. The KMS command no longer receivesPROUDAUTH_*variables (DEP-016; keys.md).Tracing (OPS-003): an incoming
traceparent's sampled flag is honored only fromserver.trusted_proxies; from other clients the trace ID is continued andobservability.trace_samplingdecides. In productionobservability.otlp_endpointmust behttps(loopback excepted); the exporter ignores proxy variables, never follows redirects and drops spans exported after shutdown.Finished sign-in transactions lose their client IP address and user agent at the next cleanup run unless
privacy.store_full_ipis set; the rows go 24 h after finishing (PRV-002).Paged lists (API-004,
TestDatabasePagedLists):GET /webhooks/{id}/deliveries,GET /notification-jobsandGET /me/security-eventsare paged in the database with cursors (newest first,limitup tolimits.max_page_size,next_cursoruntil the end) instead of returning one capped page (200 or 100 items).Verification budgets (REG-012, SEC-006,
TestVerificationBudgetPerClient): proof attempts (codes, magic links, TOTP, recovery codes, passkeys, challenge answers) also count againstsecurity.rate_limits.login_per_realm, the newlogin_per_client(default 20000 perlogin_window) andlogin_per_organization, besides the per-account and per-network budgets.Claim mappings (APP-011, OIDC-007): neither
id_token_claimsnoraccess_token_claimsmay name an OIDC standard user claim (name,email,locale, ...;422), and issuance skips such names stored by older versions in access tokens as it already did in ID tokens. Migration: rename such mappings (upgrades.md).Organization deletion (TEN-024, PRV-005): the preview adds
owned_clients; starting the deletion suspends the organization's owned projects and the active clients of those projects and of its service principals and revokes their refresh tokens, and finishing it revokes the clients' credentials and erases their secret digests and public keys. A job of the organization still running when the deletion starts stores no result and endscancelled("organization deleted while the job ran"), except the deletion job itself (data-inventory.md).A scheduled self-service deletion that falls due while the account is the only owner of an organization or owns projects, clients, service principals, resources or connections is postponed by 24 hours and recorded as
user.deletion.postponed(reasonOWNERSHIP_NOT_TRANSFERRED, metadataorganizationsandowned) (USR-020; data-inventory.md).POST /organizationsvalidates a given policy likePATCHdoes; at creationpolicy.sso_provider_idcan name only a realm-wide connection (422). Ownership transfers increment the transferred object's version, so an earlierETagno longer matches.Groups (AZ-018, AZ-019): a dynamic group's recomputation leaves out a member whose roles would conflict and records
group.member.skipped; granting or removing a role on a group raises the membership revision of the members of the group and its descendants, which changes their decision cache keys in an organization's context (ADR 0013).Authentication: polling a transaction locks it like a step; self-service passkey enrollment also clears the
configure_totprequired action; stored texts (remembered-device labels, passkey names, user agents, hook and federation error messages, JIT usernames, claim values) are cut on UTF-8 boundaries; audit metadatarecovery_restrictedandrecovery_idare no longer redacted.Organization domains: a removed or expired claim can no longer be verified (
409, "add the domain again"), and a verification racing a removal fails (ADR 0006).GET /notification-settings?organization_id=<org>returns that organization's settings andETag(forPATCHwithorganization_id), andPOST /organizations/{org}/roles/{role}/permissionsreturns the role'sETag.The redirect-URI own-origin check (APP-004) compares host names whatever the port and also covers the realm's verified custom domains; loopback hosts are exempt (oauth-oidc-profile.md).
Registering client public keys (
POST /clients/{client}/credentialswithjwks) recordsclient.credential.public_key_added(metadatakey_ids).Session-event stream (SES-019, migration
0046): pruning records the newest pruned stream position per realm, and a cursor expires (410 cursor_expired) only when stream events after it were pruned, so a caught-up consumer on a quiet realm keeps its cursor (TestStreamCursorAfterPruning).Restore reconciliation (OPS-006) erases a user deleted after the backup with the same code as account deletion (also password history, remembered devices, pairwise subjects, notification preferences, grants, automation credentials, pending recoveries, merges and approvals) (backup-restore.md).
Organization templates (TEN-017) match an existing service principal only by the exact name
<slug>-<entry>for the organization's current or a former slug, never by a name suffix.SCIM (PROV-003, PROV-009,
TestSCIMProvisioning):eqfilters onid,externalId,userNameordisplayNameare answered from indexes; other lists over more than 20,000 resources answer400 tooManyfor groups too (no truncation);ETags are derived from the resource's content, so administration-API changes make them stale (412); discovery requests are counted asget_discovery; deleting a group (also through the administration API) is one transaction that records each removed role assignment asrole.unassigned(reasonGROUP_DELETED) (scim-provisioning.md).User imports (MIG-002, MIG-006):
GET /user-import-formatsnow lists bcrypt and PBKDF2 verifiers, names, locale, the suspended state, required actions and administrator metadata undersupports, and bcrypt and PBKDF2 are no longer inunsupported(only outside their bounds); records carrying required actions now need the matching permission besidesrealm.users.import, also when the worker runs the job.security.password.legacy_hashes_until(CRED-004) now also ends imported bcrypt and PBKDF2 verifiers, not only salted SHA-256, andidentity-cli doctorcounts them.Webhook subscriptions and pre-action hooks (EXT-014, EXT-018) gain
event_versionandcontract_version(both1for existing ones, so their deliveries and requests are unchanged) andplugin_id;PATCH/DELETE /webhooks/{id}andPUT/DELETE /hooks/{id}answer409for parts a plugin manages. Every hook request now carries theProudAuth-Hook-Versionheader. Events caused by a lifecycle workflow run carry the run ID ascorrelation_id, where background work had none before./organizations/{org}/provisioning-connectorsnow accepts and lists onlyscim_inboundconnectors:"kind": "scim_outbound"(previously stored without behavior) is422onkind, andldapandscim_outboundconnectors are managed at/directory-connectors(PROV-010, PROV-011).scim_outboundconnectors created earlier appear there without a configuration; disable them before upgrading (scim-provisioning.md).clientsgainprotocol(oidc, orsamlfor the backing client of a SAML application);GET /clientsshows it, andPUT /clients/{id}answers409for SAML-backed clients (change them through/saml/applications). Password changes, password reset completion, account setup, profile and identifier edits and reactivation now pass an attribute-authority check that refuses changes a directory owns (409 conflict, USR-022); accounts without a directory link are unaffected.Configuration reconcile (SDK-012):
?mode=reconcileon/configuration/validateand/configuration/imports(CLIconfig-plan -reconcile,config-apply -reconcile) applies updates of differing clients (the whole registration from the document, keeping project, owner, default resource and service principal links), resources (members the document omits keep their values; they used to be cleared) and role permissions (added and removed within the delegation ceiling) in the same atomic import, each needing its write permission and audited asclient.updated,resource.updatedorrole.permissions.changed./validateneedsrealm.configuration.importand, like dry runs, reports missing per-change permissions (<item>: requires <permission>) and a missingexpected_policy_revisionas plan errors. Exchange policies are validated in imports and inPUT /clients/{client}/resource-grants/{resource}(max_scopesdeclared by the resource,accept_from_audiencesregistered audiences;422), lists are compared as sets, and the grant audit event recordsexchange_policy(exit-and-export.md).private_key_jwt(MCH-010, APP-009): everyaudvalue of a client assertion must be this server's token endpoint or issuer (at most two values); an assertion also addressed to another party is refused.RP-initiated logout validates
post_logout_redirect_uribefore anything is revoked, so a bad redirect leaves the session intact; the upstream-logout returnstate(FED-013) is single-use (a replay is400).The token-exchange consent ceiling (TOK-008) applies to subject tokens of consent-governed, non-machine clients, that is at the first hop; later hops are bounded by the exchange policies and grants of each hop.
Account deletion (PRV-005, migration
0048) suppresses the account's queued notifications and erases their payloads and recipients at once, for messages whose sender records the account;GET /users/{user}/deletion-statusreportsnotificationsasdoneorpendingwith a count, datesaudit_evidenceby the longest applicable retention (deployment, categories, realm) and counts only legal holds that cover the account (organization holds are mentioned separately) (data-inventory.md).Organization deletion preview (TEN-024):
active_legal_holdscounts holds on the organization, realm-wide holds and subject holds on its members.Imported audit history (AUD-013, PRV-004): records about an account already deleted keep their details when a legal hold covers them; realm and organization audit retention now also keep imported records longer.
Federation (FED-016): a connection's
kindfollows its adapter on update; changing the adapter to another protocol while identities are linked is refused with409(fieldadapter), like changing the issuer.Just-in-time provisioning (REG-009): an unverified upstream email that another account has verified is not copied to the new account, and a taken generated username gets a random suffix instead of failing the sign-in.
identity-cli config-diff(DEP-020) also marksserver.host_policy,server.public_listen,server.tls_cert_file,server.tls_key_file,storage.migration_database_url,storage.runtime_role,observability.otlp_endpointandworkers.metrics_listenas security boundary changes.Cookie-authenticated calls (SEC-002, DEP-014) accept a verified custom domain as
Originonly on the default port or the public origin's port.Operator commands (OPS-013):
restore-reconcilerecords the OS user as actor (local:<user>);migrate,revocation-logandsupport-bundleexit with an error when their audit record cannot be written (the action has already happened, and the message says so).Privileged-role detection (access reviews, migration reconciliation, exports, approvals) follows role composition at any depth, counts wildcard grants (
*,x.*) and custom permissions registered as privileged, and treats a composition of more than 64 roles as privileged.Dynamic groups (TEN-014, AZ-010): a rule matching more than 10000 members records a rule error and leaves memberships unchanged (they grant nothing meanwhile);
group.member.skippedis recorded once per new skip;member_countexcludes skipped members. Groups refuse removed or expired memberships as members (422).Flow challenges (SEC-014): the challenge provider is called outside the database transaction, after the continuation proof and the pending challenge are checked; its answer is applied in the step's transaction.
proudauth_security_signals_total(OPS-004) counts an audit event when its transaction commits; rolled-back or retried attempts are not counted (audit write failures still are, immediately).identity-cli bootstrap -ttloutside 1 minute to 24 hours is refused (it used to become 1 hour silently; DEP-005).Custom domain verification succeeds only for the pending challenge that was checked; a removal during the DNS lookup wins (
409).Route labels of metrics, spans and request logs (OPS-002) are the registered route patterns (for example
/api/v1/realms/{realm}/users/{user}), orunmatched. Idempotency records (API-008) hash method and path with the body, so a key reused for another resource is a mismatch (422), never a replay.Residency (PRV-003): organization mail servers and upstream identity provider URLs outside
privacy.residency.allowed_destinationsare refused when saved (422), not only when contacted.Decision cache key (AZ-019): the fingerprint covers the roles of ancestor groups and the versions of composed roles; group memberships with a validity window make decisions uncacheable; a role's permission change invalidates holders of roles composing it (any depth) and members of groups holding them.
Rate limits: password attempts per client use
security.rate_limits.login_per_client(default 20000 per login window; it wastoken_per_client× 10, about 6000); identifier-verification sends count against the per-client and per-organization send budgets.Inactive accounts (suspended, locked, pending verification or deletion, deleted) and disabled or deleted service principals hold no permissions anywhere, in decisions and in job re-checks.
user.bulkjobs check the permission of each action (no longer a blanketrealm.users.write), at the request and in the worker; organization deletion jobs run only while the requester still holdsorg.delete(else they dead-letter and the organization staysdeletinguntil an authorizedDELETEqueues the job again).Job downloads: audit and realm data exports are served as
application/x-ndjson(export.ndjson), other results asapplication/json(export.json); the download permission is checked again when the token is redeemed (a403does not spend the token), andjob.result.downloadedrecords the actual actor type.Invitations:
limits.invitation_resendscounts resends only (one more send than before); a negativettl_hoursis422; declining an expired invitation is400 invitation_invalidand marks it expired; list cursors are bound to their filters (400when reused with others); the bulk limit error names the configured maximum.Passkey self-service enrollment requires user verification while
security.mfa_floor.admin_phishing_resistant(or the realm'smfa.admin_phishing_resistant) is set.Runtime role setup also revokes
CREATEon schemapublicfromPUBLIC(effective when the migration role owns the schema or is a superuser).The realm data export includes
owner_user_idfor authorization policies and hooks, andhotpin each account's credential summary.SCIM: Group filters accept URN-qualified attribute names; deletes check
If-Matchunder a row lock; a connectorPUTthat loses to a concurrent update answers412.retention.cleanupruns every step even when one fails and reports the failed steps together; KMS commands that leave a child process holding their output are waited for at most 1 more second.
Fixed#
Local image secret reinitialization (DEP-002, DEP-004): unchanged credentials now recover the container-readable file mode while preserving their bind-mounted inode. PostgreSQL startup also refuses automatic image pulls. Deployment regressions cover private generation, idempotence, permission repair, the PostgreSQL 18 volume root and Swagger contract drift.
Hosted CI E2E deadline (QA-002, QA-008): a full race-detector run reached test 378 of 403 when the 20-minute package deadline expired; the active test had run for only two seconds and no assertion failure preceded the alarm. The package deadline is now 30 minutes, with a 45-minute test-job cap and verbose progress for diagnosis. The complete suite, race detector, password costs and test assertions remain required.
Development Compose startup on PostgreSQL 18 and native Linux (DEP-002, DEP-004): the data volume now mounts
/var/lib/postgresql, as required by PostgreSQL 18's versioned data layout. Generated secret files are readable through the non-root containers' read-only mounts while their host directory remains owner-only; repeated initialization preserves keys and passwords. Deployment regressions cover the mount, permissions and idempotence. Existing database volumes require an explicit migration.CI static analysis failed on Go 1.27 (QA-002, QA-008): the old Staticcheck 2025.1.1 exporter supports data version 2 and cannot read the compiler's version 5. CI now pins the tested stable 2026.2.2 release. Twenty real findings are resolved: unused helpers/fields and assignments, redundant formatting/loops, and deprecated coordinate access in the software passkey fixture. Exchange expiry continues to use the injected clock. The old failure and passing replacement are recorded with source hashes; no tagged release is affected.
Session-lifetime test clock calculation (TEST-032): the regression now measures elapsed time from its injected clock's starting value, removing a sign error introduced during the Staticcheck cleanup and avoiding dependence on wall time. Three race-detector repetitions and the final full check pass (716 top-level tests, 562 subtests); production session and refresh limits are unchanged. All 11 CI fuzz targets also pass.
Offline bundle failures could destroy prior assets or hide export errors (DEP-017, QA-008): builds now validate version labels, stage privately, preserve/restore the previous archive and checksum on failure, refuse concurrent writers and clean up on interruption. Explicit image export and per-file hashing propagate failures instead of hiding them in POSIX shell pipelines. Filenames containing spaces keep their checksums, and macOS AppleDouble metadata is excluded from the distributed archive. Executable fault, signal, concurrency and archive-integrity regressions cover these boundaries. Affected: earlier untagged development tooling; no schema change.
Floating container base references (DEP-003, SEC-016): the builder and non-root runtime now use verified, locally cached OCI index digests while retaining exact version tags. The release toolchain contract requires immutable references; frontend/Action pins, vulnerability review and off-host release/image reproduction remain open.
Offline federation guidance omitted production CIDR allowances (OSS-005, FED-017): internal upstream providers may use explicit
federation.egress_allowlistand a private CA in production; the broad private-network bypass remains rejected.Concurrent and failed metadata/JWKS fetching bypassed retry bounds (FED-017, NFR-006): OIDC discovery/key fetches, workload keys and Shared Signals keys share a bounded fetch and a one-minute retry cooldown per process/cache identity. Previously a concurrency-32 burst made 32 fetches; three invalid-response bursts made 96 instead of two. Caller cancellation leaves other waiters' fetch alive, invalidated old responses cannot overwrite new caches, DNS shares the fetch deadline and temporary metadata clients close idle traced connections. TLS, egress, signature checks and workload transport/status fallback remain enforced. Affected: earlier untagged development builds; no schema migration. See upgrades.
Literal audit prefixes and filter-bound cursors (AUD-006, AUD-011, API-004): earlier development builds interpreted
_/%/backslash as SQL LIKE instructions and accepted audit cursors after filter/route changes. Prefixes now match literally; cursors bind canonical filters, route, realm and authorized organization. Regression fixtures prove exact compound pages with newer evidence inserted between pages, foreign scope and changed-filter refusals. Restart existing audit pagination after upgrading; records and schema are unchanged (upgrades).SCIM Group filters issued per-group database reads (PROV-002, PROV-008, NFR-004, NFR-006): list queries now batch scoped metadata and stream ordered members, retaining one raw group collection and the requested page. The regression observed 17 queries for 8 Groups and 129 for 64; both now use three. Member-dependent projection, full counts, individual-read ETags, missing/foreign mappings and database-error propagation retain regression coverage. Single Group reads also qualify organization/realm membership scope. Affected: untagged development builds before this fix; no schema migration.
Passwordless OTP aliases bypassed the shared account budget (TEST-021, NFR-006): an email/SMS login challenge now supplies its bound account solely for budget selection, without authenticating the transaction. Previously rotating email, phone and HOTP for one account allowed 28 evaluated wrong proofs in a concurrency-32 burst instead of ten. Phantom challenges retain identifier budgets; database lookup errors fail closed. Affected: untagged development builds before this fix. No schema change.
Partial directory failover suppressed deprovisioning (PROV-012, PROV-014, PROV-017, NFR-007): each endpoint scan resets its deletion presence set, so only a complete successful source view determines missing memberships. Previously users seen before a broken connection stayed active even when absent from the fully scanned replacement. Regression tests reproduce this on full and incremental-to-full scans; complete transport exhaustion still cannot delete from partial data. Affected: untagged development builds before this fix.
Expired job workers could overwrite replacement results (DEP-011, PROV-012, NFR-007): progress and terminal writes now require the unexpired worker/attempt claim. Directory mutation transactions lock that claim before changing accounts, links, Groups, delivery state or final evidence. Previously a stalled worker changed a successfully recovered sync to failed and emitted a second completion audit. Regressions retain the replacement's state, cursor, protected job results and exact audit counts. Final-write errors propagate instead of reporting completion. Affected: untagged development builds before this fix. Drain old workers before replacement; upgrade notes.
Group deletion could stall connector updates and strand external IDs (TEN-013, PROV-004, PROV-007, PROV-008, DB-014): local deletion, SCIM writes and directory Group application now lock the group before its mapping and members. Inbound/directory links end atomically with local deletion, and reprovisioning repairs legacy orphan links. Live groups are not adopted; outbound links remain until remote deletion is delivered. Pre-fix regressions reproduce a membership lock timeout and
409recreation failure; replica, LDAP and actual SCIM transport regressions prove the correction. Affected: untagged development builds before this fix.Benchmark SQL commands could consume a streaming caller's input (NFR-008):
bench-local.shuses interactive Docker stdin only for explicit stdin SQL, keeping later caller validation/cleanup commands intact afterpsql -c.TestBenchRunnerSQLKeepsCallerInputexercises command and heredoc forms with a consuming Docker stub. Affected: the untagged local runner before this fix.Refresh and session revocation held locks in opposite orders (SES-007, NFR-007): session-bound refresh now locks its session before the family; account-wide revocation visits session IDs in order. Initial concurrency-32 requests reached the 30-second deadline. Database-backed replica regressions require completed revocations and no post-commit successor or active access token. Affected: untagged development builds before this fix.
Grant revocation left JWT authority active online (SES-008, SES-021, SES-023, TOK-010): JWT introspection, UserInfo and IdP user APIs now check their realm/subject/client-bound active, unexpired grant. Token exchange checks and retains the originating grant. Previously an offline JWT remained active after confirmed refresh reuse; unrelated grants and SSO sessions still survive client-local grant revocation. Regression tests cover all three replicas, missing/foreign-client grant links, exchanged descendants and fail-closed grant-store errors. Affected: untagged development builds before this fix.
Concurrent ownership transfers upgraded incompatible shared locks (TEN-006, NFR-007): transfers now acquire the exclusive organization lock directly, then check active state and current authority. Non-dry template application uses the same mode for policy writes. The concurrency-32 regression failed at the 30-second deadline before the fix; it now leaves one active owner and rejects the former owner's competing transfers. Ordinary grant issuance retains shared locking. Affected: the untagged development build before this fix.
Concurrent role deletion reported repeated transitions (NFR-007): a delete affecting no row now returns
404before changing revisions or recording audit/outbox evidence. Competing expiry workers skip an already removed assignment and count only real expirations. The regression previously observed 27 successful deletes of one grant; repeated replica tests now require one transition and exact live permissions, holder revisions and evidence counts. Affected: the untagged development build before this fix.SCIM User scans performed four database round trips per candidate (PROV-002, PROV-008, NFR-004, NFR-006): 32 concurrent 10000-user boundary-filter scans timed out without a successful result. Scoped mappings, user/identifier data and memberships now load in bounded batches; only the requested page is retained while every match is counted. A real-query tracer regression fails for the old 33/257-query reads and now observes five queries at both cardinalities, with identical individual resource data/ETags. Batch reads preserve realm scope, identifier order, deletion and failure semantics. The initial failed reference run remains published alongside the passing corrected measurement: 677 exact bounded searches complete without transport errors, with parser-overflow and unchanged connector-budget refusals also verified. Affected: the untagged development build before this fix.
Initially inactive SCIM users failed with a default role (PROV-004, PROV-005, QA-008): the connector now creates the membership, assigns its approved default role and suspends it in the same transaction. An active membership is never exposed outside that transaction. The regression checks the suspended membership and retained default role, then ETag-bound reactivation without a duplicate grant. General role assignment's active-membership requirement remains enforced. Affected: the untagged development build before this fix.
SCIM read failures appeared as complete empty/partial lists (PROV-002, NFR-006, QA-008): user and group lists now propagate database read errors instead of silently dropping candidates. User/identifier failures also return an error instead of falsely reporting
404. Deleted or concurrently removed resources remain invisible. A real PostgreSQL regression locks each relevant table and forces read timeouts, then checks error documents, recovery and deleted-account behavior. Affected: the untagged development build before this fix.Benchmark interruptions and cleanup failures could report success (NFR-005, NFR-008): the CLI now keeps usable partial JSON while returning a failure, and results/envelopes record
complete. Encoding/write errors are propagated with the original failure. The local runner requires completed runs/windows and preserves the configured host label on partial evidence. Soaks exclude preparation/warm-up from the measured budget and shorten their last window instead of overrunning a nonmultiple duration. Unit and real-process signal regressions cover these lifecycle failures; historical reference JSON remains unchanged. Details: benchmarks.md.Webhook benchmark preparation crossed unfinished seed work (NFR-004, NFR-008): full-cardinality imports and assignments can leave an outbox backlog behind a default worker. An unmeasured public-API readiness cycle now proves callbacks and delivered records before timing, with its wait recorded. Measured timeouts and worker budgets remain unchanged.
TestBenchWebhookReadinessCrossesSetupBacklogchecks that delayed setup callbacks cannot satisfy a later operation.Empty membership pages violated the API contract (API-004): a newly created organization or the last live member's removal could yield
{"items":null}. Member listings now return an empty array and an empty cursor as specified.TestEmptyMembershipPagesAreArraysreproduces the earlier failure and checks both default and active-state listings.Benchmark boundaries lost completed mixed operations (NFR-008): a component's
:okresponse completed at cancellation is now counted, consistently with standaloneok; interrupted operations remain excluded.TestBenchCountsCompletedMixedOperationAtBoundarycovers both cases.Soak fixture preparation (NFR-005): the configured concurrency is now available before preparation, so passkey, SCIM and mixed scenarios can reserve their per-worker fixtures and reuse them across windows.
TestSoakPreparesAllWorkerFixturesOncecovers this tool defect.Local SMTP benchmark configuration: the runner supplies the required sender address when switching from the bootstrap mailbox to SMTP;
TestBenchSuiteSmokevalidates that complete configuration before loading the actual transport.Benchmark import polling: missing job IDs and HTTP errors now fail preparation immediately instead of polling an invalid resource for up to five minutes. Controlled missing-ID and 401/404/503 fixtures cover this behavior in
internal/bench.SDK bearer paths ignored sender bindings: the unpublished 0.2.0 Go, Node.js, Dart and PHP SDKs could accept a valid JWT or active opaque token containing
cnfwithout checking possession. Their token-only verifier, introspection, bounded-state and metadata paths now reject it withsender_constraint; unknown/malformed bindings are also refused. Four new shared signed fixtures and opaque-token tests cover every SDK. Go applications using bound tokens must migrate toSenderVerifier; request-level sender verification is not yet offered by the other three SDKs. Ordinary unbound bearer tokens keep their previous behavior.Go introspection accepted ambiguous or oversized JSON: response parsing now rejects duplicate members, trailing values and responses above 64 KiB, with
ErrUnavailable. Controlled regression responses cover each case; this affects the unpublished development SDK.Go SDK requests followed HTTP redirects: issuer metadata/JWKS, OIDC token/revocation requests, introspection, revocation-state replay and administration calls now return redirect responses without forwarding credentials or sensitive bodies. A shared transport wrapper preserves the caller's client settings and a real HTTP regression proves that 301/302/303/307/308 destinations receive no requests. This affects the unpublished 0.2.0 SDK (SDK-008).
Timezone regression test raced with HTTP cleanup:
TestTimeAndUnicodeInvariantsnow starts an isolated subprocess withTZ=America/New_Yorkinstead of mutatingtime.Localwhile HTTP and database goroutines read it. Its DST/Unicode checks still run against a real isolated PostgreSQL database under the race detector.Identifier uniqueness stress test counted successful no-op updates: contenders now choose a stable identifier before racing, require one affected row on success and verify the persisted count. An
OFFSETinto the shrinking set of unverified identifiers previously let a late transaction update zero rows and falsely report two winners. The verified identifier unique constraint remains unchanged.DPoP request binding accepted ambiguous headers and method casing: the token endpoint and protected APIs now refuse multiple DPoP headers, and the proof's HTTP method must match exactly, as required by RFC 9449.
TestDPoPBoundTokensreproduced both earlier acceptances with controlled proofs and retains duplicate/case-shadowed claim refusals. This affects the untagged development server; proofs with lowercasepostfor aPOSTrequest must be corrected by clients (TOK-019, TEST-067).The oversized-input benchmark could upload bodies despite
Expect: 100-continue(NFR-006): Go's one-second continue timeout fell back to sending the body while waiting for a delayed 413. The probe now offers headers with the declared length, waits through the request deadline, and uses a reader that cannot transmit body bytes; asking for the body reportsinvariant_violation_body_requested. Regression tests cover a delayed refusal and a server sending100 Continue(TestOversizedWaitsForHeaderRefusal,TestOversizedDetectsContinue).Benchmark seeding exceeded the default request limit (NFR-004):
make bench's 10000-user dataset failed with HTTP 413 because a batch of 1000 users with password verifiers and organization memberships exceeded 256 KiB.identity-bench seednow bounds each import by both 1000 records and 192 KiB; the server's request limit stays unchanged (TestSeedImportsFitDefaultBodyLimitininternal/bench).SAML connections refused responses that repeat an attribute name (FED-014, SAML-006): the broker refused a whole response (
upstream_invalid_response, audit reasonduplicate_attribute) when twoAttributeelements had the sameName. Keycloak's default SAML client scoperole_listsends oneRoleelement per role, so no Keycloak user holding roles (every user has the realm's default roles) could sign in through a SAML connection to Keycloak. Repeated names are now one multi-valued attribute, which never fills a single-valued field; the connection'ssubject_attributeandemail_attributemust still appear once, and a repeated one is still refused as ambiguous (TestSAMLBrokersubtest "repeated attribute elements (Keycloak role list)",scripts/interop-keycloak.sh; saml.md).Outbound SCIM requests were refused by ZITADEL (PROV-010): every request carried
Accept: application/scim+json, application/json, which ZITADEL v4 answers with400 Invalid accept header, so every delivery to it dead-lettered asrejected. Requests now name one media type,Accept: application/scim+json(RFC 7644 section 8.1); responses are still read as JSON whatever theirContent-Type(TestOutboundProvisioning, whose SCIM double now refuses anAcceptlist as ZITADEL does;scripts/interop-zitadel.sh).Email changes did not reach outbound SCIM services until a reconciliation (PROV-010): adding, verifying, making primary or removing an email address or username did not change the account's
updated_atand published no outbox event, so the change scan never saw it; a downstream kept the old address (and an account refused for want of a verified address was never retried) until a reconciliation, which is not scheduled by default. Identifier changes now mark the account changed, and the next scheduler pass delivers it (TestOutboundProvisioning; outbound-provisioning.md).A self-service deletion request could not be cancelled by its owner (USR-012): the request revoked every session and a
pending_deletionaccount could not sign in, soDELETE /me/deletion-requestcould never be called, and thedeletion_schedulednotice ("sign in and cancel") was never sent. The request now queues that notice in its own transaction, and a pending-deletion account can sign in with its usual factors (MFA included) into a transaction whose only action iscancel_deletion(POST /auth/transactions/{id}/cancel-deletion, optionsdeletion_scheduled_at,deletion_requested_at); no session, code or token is issued while the deletion is pending. Cancelling makes the account active (audituser.deletion.cancelled,via: sign_in) and the sign-in completes; otherwise the deletion completes when due (TestDeletionCancelAtSignIn,TestDeletionCompletesWithoutCancel; headless-authentication.md).A refresh could shorten the shared SSO session (SES-002, SES-006): a refresh set the session's idle expiry to the refresh family's window from now, so a client with a short
token_policy.refresh_idle_secondscut the session's idle period for every application. A refresh now only moves the session's idle expiry later and never touches its absolute expiry (TestRefreshNeverShortensSession).A missing pepper version was hashed as an empty password (CRED-009): a verifier naming a pepper version the keyring lacked was checked against Argon2id of the empty input instead of failing. It now fails closed with
credential.ErrPepperUnavailable, logged and audited ascredential.password.pepper_unavailable(TestMissingPepperVersionFailsClosed; keys.md).A sign-in for a resource that requires an organization, by a user with no active membership, offered
select_organizationwith an empty list that could never be answered; the transaction now stops with the blocking reasonno_organization(TEN-005,TestSignInWithoutAnyOrganization; headless-authentication.md).Lost contact address recovery could not reach the new address (MFA-008, USR-004, USR-005): the runbook told administrators to add the owner's new address, override its verification and run assisted recovery, but there was no administrator API to add an identifier or make one primary, and assisted recovery always mails the primary address, so the setup link went to the lost one unless the owner, who could not sign in, changed it themselves. Administrators holding both
realm.users.verification.overrideandrealm.users.credentials.manage, from an MFA session with recent authentication and never for their own account, now add an email address with a reason (POST /users/{user}/identifiers: unverified, not primary,409when another account verified it, audited asuser.identifier.added) and make a verified one primary (POST /users/{user}/identifiers/{id}/primary:422for an unverified address, audited asuser.identifier.primary_changed); the previous verified primary address receives anidentifier_changednotice for each change. Assisted recovery no longer mails its setup link to an unverified primary address: an immediate request answers409, scheduling one is refused the same way, and a due request whose primary address is unverified is cancelled withcontact_unverified. Before, recovery mailed the link to the primary or any other listed email address, verified or not, and completing that setup marked the address verified (account-recovery.md,TestLostContactAndFactorRecovery,TestAdminIdentifierChangeGuards).A request to the API without any credentials was answered with
WWW-Authenticate: Bearer error="invalid_token"; it now gets a bareBearerchallenge (RFC 6750 section 3.1), and CORS responses exposeWWW-Authenticateto registered origins (INT-001,TestIdPCORSAndChallenges).Incident runbooks that did not match the server (OPS-016): found by the new runbook tests (
TestRunbookConnectorCompromise,TestRunbookWebhookOutage,TestRunbookRootKeyCompromise,TestSharedRevocationAcrossNodes). A disabled connection refuses sign-in and linking with422 validation_failed, notfeature_disabled; webhook failure categories are status classes such ashttp_500, nothttp_5xx(incidents.md, monitoring.md, webhooks.md); the root-key drain checks in keys.md never reached zero after an emergency rotation or a webhook secret rotation (they countedcompromisedsigning keys and expired previous webhook secrets) and missed HOTP seeds, SAML signing keys, organization mail-server passwords, connector credentials, shared-signals stream headers and protected job input; TOTP re-enrollment now uses enroll and activate (DELETE /me/totpis refused for accounts that must keep a second factor); an emergency signing-key rotation is documented as taking up to the 5-second key cache on other server nodes, which until then also sign new tokens with the compromised key; and an account created just in time through a compromised connection is suspended, because its only link cannot be removed (409 last_sign_in_method).Role assignments to a group at realm or project scope were stored but never evaluated; they are now refused with
422onscope("groups hold organization roles only"), since a group belongs to one organization (AZ-010,TestGroupsHoldOrganizationRolesOnly).Emailed codes ignored a
ui_localespreference list (NFR-009): the locale of a message sent during an authentication transaction is the request'sui_locales, a space-separated list, but template lookup treated the whole list as one tag, soui_locales=fr-CA esfell back to English although a Spanish template exists. Each listed tag (then its base language, at most five tags) is now tried in order before English (TestAuthorizationLocale,TestLocaleFallbacks).Headless error table (NFR-009):
403 account_unavailable, returned when the step that would complete sign-in finds the account suspended, locked or deactivated meanwhile, was missing from the Errors table of headless-authentication.md;TestHeadlessErrorCodesDocumentednow keeps the table and the transaction API's problem codes in step.Key provider outage answered
500 server_error(TEST-059, SEC-009): when the active signing key existed but could not be unwrapped (keyring, KMS or HSM unavailable), token requests failed with500 server_error; they now answer503 temporarily_unavailablelike a missing active key (token.ErrKeyUnavailableis anErrNoActiveKey), still without consuming the code or refresh token presented, and an ID token that cannot be signed answers the same. The signing-failure metric keeps the reasonkey_unavailable(TestSigningKeyOutageDuringIssuance).Restore reconciliation could apply nothing on a retried transaction (TEST-060): the revocation log was read inside the database transaction, so a retry after a deadlock found the reader drained and committed without the records. The log is now read (and validated: a malformed line aborts without effect) before the transaction. A
password_changedrecord whose restore already holds the current password (an overlapping log) no longer revokes the user's sessions.Project-scoped role assignments were never evaluated (AZ-002): roles assigned with
scope.type: projectwere stored but ignored: token scope policy and the decision API evaluated only realm and organization assignments, the decision request could not name a project, and the permission views were realm or organization only. Project assignments now count, for their own project only, wherever a resource belongs to a project: scopes withrequired_permissionsof a resource with aproject_id(every grant type, personal tokens and API keys), the decision API with the new optionalresource.project_id(on/check,/check-batchand simulation; validated: an unknown project denies withUNKNOWN_PROJECT, one that does not belong to the decision's organization, owned or shared through an active project grant, withPROJECT_ORGANIZATION_MISMATCH), andGET /me/permissionsandGET /subjects/{subject}/permissionswith?project_id=. They apply only while the project is active and never at organization or realm level, for another project, or in an organization the project does not belong to; explicit denies still override them, and decision cache keys include the project (TestProjectScopedRoles).Composite roles across projects and beyond the evaluation bound (AZ-011): a project role could compose another project's role, carrying that project's grants; this is now refused with
422. A composite role whose composites exceedlimits.max_role_expansionfailed with500, and the creation check bounded only path depth while enumerating every path, so a diamond-shaped composition was expensive to check; creation now checks cycles and the bound (the role and every role it reaches) in one linear walk and refuses with422. Roles that together exceed the bound for a subject still fail closed at evaluation (EVALUATION_ERROR) (TestCompositeRoleBounds,TestCompositeRoleGraphChecks).Backend enforcement of new routes (AZ-008): nothing guaranteed that a route registered through the authenticating wrappers (
a.admin,a.tenant,a.self,a.protected) checked a permission. A contract test now requires every such route to reach a permission check or a service operation that authorizes the caller, with an explicit, reasoned list of self-service and addressee-only routes (TestAuthenticatedRoutesCheckPermissions).API key and personal token last use (MCH-005, MCH-007): introspection never recorded
last_used_atfor API keys and personal access tokens: the update compared the column with an untyped$2 - interval '1 minute', which PostgreSQL resolves as interval arithmetic, so the statement failed and its error was ignored. Every credential therefore looked never used inGET /api-keys,GET /me/personal-tokensand the credential inventory, whoseunusedflag fired for keys in daily use. The parameter is now typed and the last use is recorded at most once a minute (TestScopedAPIKeys).identity-cli assign-rolefor realm roles (SDK-003): without--orgthe command sent a realm scope naming the realm by its name (main), which the API refuses because a realm scope carries the realm's ID; realm roles could not be assigned with the CLI. It now sends no scope, and the server applies the realm of the request (TestAdminCommandsCallAdministrationAPI,TestAdminSDKTypedOperations).ZITADEL adapter capabilities (SES-022, MIG-010): the ZITADEL entry of
GET /migration-adaptersandidentity-cli migrate-from --listdid not list sessions among what is unsupported, although no adapter migrates sessions and the conversion report already counted them as unsupported; it now lists "sessions and tokens (users sign in again)", and every registered adapter is checked for it (TestEveryAdapterReportsSessionsUnsupported).Hook diagnostics for answers outside the contract (EXT-011, EXT-017, TEST-053): an
authorization.decisionhook answering with more than five obligations was denied (HOOK_INVALID_RESPONSE) but recorded as a successful call, so its diagnostics stayedhealthy; it is now a failure with categorymalformed_response, and a denial carrying more than five obligations isHOOK_INVALID_RESPONSEas well. Atoken.claimsanswer setting a protected claim or exceeding 20 claims or 4 KiB is now categorizedmalformed_responseinstead oferror, and a transport timeout that is not a context deadline is categorizedtimeout(TestHookBounds,TestClaimsAnswerBounds,TestErrorCategories).Per-organization job cap under concurrency (DEP-015, TEN-018):
workers.max_jobs_per_organizationwas checked inside each claim, so two workers (or runners of one worker) claiming at the same moment each counted the other's job as not yet running and together exceeded the cap. Claims are now serialized by a database advisory lock while the cap is on (TestWorkerPoolBoundsAndTenantShares).Duplicate notification delivery under concurrent workers (DEP-011): the notification claim checked state and lease only in the snapshot it started with, so a worker could lock a message another worker had just claimed (or already delivered) and send it again. The claim now re-checks state, due time and lease on the row version it locks (
TestReplicasShareOneTimeState,TestNotificationLeases).Duplicate webhook and outbound provisioning deliveries, early workflow steps under concurrent workers (DEP-011): the webhook and outbound SCIM delivery claims had the same flaw as the notification claim: state, due time and lease were checked only in the claim's starting snapshot, so two workers could claim the same delivery and send it twice (8 of 90 webhook events and 2 of 42 SCIM creations went out twice in a two-process burst). The workflow-run claim re-checked only the lease, so a run another worker had advanced and parked for later (a retry backoff, a wait) or finished in the meantime was claimed again and its next step (an endpoint call) ran early. All three claims now re-check state, due time and lease on the row version they lock or update. The other claim loops (outbox dispatch, back-channel logout and SSF push deliveries, jobs, workflow schedules and the expiry sweeps) already checked their conditions at the lock and are unchanged (
TestWebhookDeliveryClaimedOnce,TestOutboundDeliveryClaimedOnce,TestWorkflowRunClaimRechecksState).Default scopes were only a declaration (APP-007): a scope's
is_defaultwas stored and returned but never used, so a request naming a resource without any of its scopes (scope=openid&resource=..., or a client-credentials request withoutscope) got an access token with no scopes. Such a request is now made with the resource's default scopes, as far as the client's resource grant lists them, and they pass every check a requested scope passes (consent, step-up, the organization's scope ceiling,required_permissions); a default the client, user or organization does not qualify for is left out without failing the request. Requests that name a scope of the resource are unchanged, OIDC scopes are never defaulted, a refresh never adds defaults declared after the authorization, and token exchange keeps the exchange ceiling as its default (oauth-oidc-profile.md,TestDefaultScopeGrants).Support bundle (OPS-010):
storage.migration_database_urlwas copied into the bundle's configuration (and intoidentity-cli config-diffoutput) unredacted, including a literal password; it is now redacted likestorage.database_url. The bundle also reports the release it was built from (version):-X main.versionin the image, release and bundle builds had no variable to set (TestSupportBundleIsSanitized,TestRedactedHidesSecretValues).Custom HTML notification templates (MSG-004):
POST /templateschecked only which variables a template names, so an HTML body could put a client-controlled value (the sign-in device, an inviter's display name) in a link (<a href="{{.device}}">), build a URL around a code, place values in scripts, styles or event handlers, or be HTML thathtml/templatecannot render, which made every delivery of that event fail. The HTML body must now render, a URL attribute may hold only exactly{{.link}}(built by the server), and values in scripts, styles,on*attributes,<meta>elements and tag or attribute names are refused (422). Templates saved earlier are not re-validated (TestValidateTemplate,TestCustomTemplateSafety).Certificate sign-in attempt budget (PROV-016): before
identifythe step's per-subject budget was keyed by an empty account, so every certificate attempt of a realm shared one budget (one client could lock everyone out of certificate sign-in). It is now keyed by the certificate's thumbprint and the client's /24 or /48 network until the account is known, and by the account afteridentify(TestCertificateSignIn,TestCertificateBudgetSubject).mTLS trust anchors and certificate checks (MCH-014, PROV-016): with
revocation: crl, every certificate of the chain below the anchor is checked against its issuer's CRL (a revoked intermediate now refuses its leaves; a leaf issued by an intermediate is no longer looked up by serial in the anchor's list, and is refused asREVOCATION_UNAVAILABLEbecause the anchor holds no CRL for it). A CRL withoutnextUpdate, which made every certificate of the anchor fail, is refused at upload (422). A CRL update needs a greater CRL number (or, without numbers, a laterthisUpdate) or the identical list: an equal number with other content is422, and the comparison runs on the locked anchor. A removed anchor can be trusted again (a new anchor; migration0086makes only active anchors unique). A binding created fromcertificate_pemis verified against the anchor named inanchor_id, not any user anchor (TestCertificateSignIn,internal/certauth/certauth_test.go).HOTP (MFA-013): a recovery-restricted session may enroll and activate HOTP (
/me/hotp/enroll,/me/hotp/activateare among its permitted writes), and activation restores the session and the account as TOTP's does.POST /me/hotp/resyncneeds a session that completed MFA (403 mfa_required), as regenerating recovery codes does. A TOTP or HOTP the realm'smfa.allowed_factorsdoes not allow satisfies nothing, and no longer counts as an enrolled factor for adding a factor (MFA-006), changing the password (CRED-006) or the last-factor guard (TestHOTP,TestCodeFactorsFollowAllowedFactors).Relationship sharing and listing (AZ-023, AZ-027): an expired share was never stored as
expired(the update was rolled back with the refused acceptance) and kept blocking a new offer. The refused acceptance now stores it in its own transaction,lifecycle.expirystores every pending or accepted share pastexpires_atasexpired(removing an accepted share's tuple; auditauthorization.share.expired), listings show and filter such shares asexpiredat once, and an expired share no longer counts as a duplicate.list-objectsclamps alimitabove 500 to 500 (it was reset to 100) and setstruncatedwhen an object's own evaluation reaches a bound and it is left out (TestRelationshipAuthorization).Elevation expiry evidence (AZ-024): the expiry sweep now records the documented
authorization.elevation.expiredevent (actorsystem) when an active elevation ends at its deadline (TestJustInTimeElevation).Ownership transfer of extensions (DOM-009, APP-014, MCH-007):
POST /users/{user}/ownership-transferalso moves the pre-action hooks, webhook subscriptions and authorization policies the account owns (plugin-managed ones keep their plugin), and needs their write permissions too. NewPOST /hooks/{id}/ownership-transfer,POST /webhooks/{id}/ownership-transferandPOST /policies/{policy}/ownership-transfer({"owner_user_id"}, optionalIf-Match) change one object's owner, also for objects without one (TestOwnershipTransferCoversExtensions).Account deletion coverage (PRV-005): every message about an account now records it (
notification_jobs.user_id: password-reset, verification and magic-link messages, security notices and any message to one of the account's addresses), so deletion purges them; messages queued earlier are found through their user or challenge reference. Deletion also deletes the account's certificate bindings and the relationship tuples naming it, revokes the shares to or from it (removing the tuple of an accepted share it made) and clears their messages, and revokes its elevation eligibilities, cancels pending requests and ends an active elevation (TestAccountDeletionCoverage).Privileged-role checks agree (MIG-005): the migration reconciliation's SQL now counts a composition of more than 64 roles as privileged, as
IsPrivilegedRoledoes (which now counts only roles that exist) (TestPrivilegedRoleCapConsistent).Configuration reconcile of a client whose type differs (SDK-012): under
mode=reconcilesuch a client, when it also differs in a reconcilable field, is a plan error ("client_type is A and cannot change to B") reported by validation and refused up front, instead of being planned as an update that failed with422and rolled the import back (TestConfigReconcileClientType).Introspection reported every token as
token_type: Bearer; a DPoP-bound token is nowDPoP(RFC 9449 section 6.2), as in the token response (TestDPoPBoundTokens).Token type of sender-constrained tokens (TOK-019, RFC 8705, RFC 9449): certificate-bound access tokens (
cnf.x5t#S256) are reported astoken_type: Bearer, no longerDPoP; only DPoP-bound tokens areDPoP. Token exchange now reportsDPoPfor a DPoP-bound result instead of alwaysBearer(TestMutualTLS,TestDPoPBoundTokens).Encrypted responses (TOK-020, OIDC-013): when a client registered response encryption but has no key that can receive it (or the realm has no active signing key), the token request is rolled back: the authorization code, device code or refresh token is no longer consumed, so a retry gets the same
unauthorized_client(ortemporarily_unavailable) answer instead of being treated as code replay that revoked the grant, and the code can be redeemed once a key is registered. Encrypt-only UserInfo responses no longer carry an emptyctyheader; only nested JWTs namecty: JWT(TestEncryptedResponses).Workload federation (MCH-011, ADV-004): an assertion's
jtiis spent only by a request that is otherwise accepted, inside the issuing transaction (still exclusive: of two concurrent uses one gets a token), so a request refused for its resource, organization or scope no longer burns the assertion. The worker's retention task deletes expiredworkload_assertion_jtisrecords (stepworkload_assertion_jtis). The configuration-document schema now acceptstoken_endpoint_auth_method: nonefor machine clients whose only grant isjwt-bearer, as the server and the configuration export do (TestWorkloadFederation,TestConfigDocumentClientAuthMethods).Agents (MCH-017): introspection of an agent's API keys returns
agent: true, as for its access tokens. Organization templates no longer grant an agent a privileged role (or any subject a role that requires approval) without the second administrator direct assignment needs: such a grant is a plan conflict and the application fails with409until the grant is approved through/role-assignments(TestAgentPrincipals).Dynamic client registration (APP-016, APP-017): a registrant's
PUTis one transaction, so a refusedrotate_secret(409) no longer replaces the registration access token and leaves the registrant without management access;limits.client_secrets_maxcounts only secrets that have not expired (for administrator rotations too). A registrant'sPUTkeeps the settings an administrator controls (token policy, claim mappings, back-channel logout URI, pairwise subject type and sector, consent and offline-access settings, origins and the like) instead of resetting them. Registrant-suppliedcontactsno longer receive credential-expiry mail.POST /clients/{client}/reactivate(and the emergency control'senable) refuses a client whose registration is pending approval (409); approval and the client's activation are one transaction. Expired initial access tokens are listed withstate: expired(TestDynamicClientRegistration).FAPI 2.0 (ADV-001): an organization switch (
POST /me/organization-grants) no longer issues authorization codes outside PAR (and without the 60-second lifetime) tofapi2clients: clients that must use pushed authorization requests (fapi2,high_assurance,token_policy.require_par) get403and switch with a new pushed request naming the organization (TestFAPI2Profile).Configuration and API contract:
identity-cli config-diffmarksserver.mtls_listen,server.mtls_originandserver.client_cert_headeras security boundaries (TestDiffMutualTLSBoundaries). The OpenAPIOpenIDConfigurationschema lists every member discovery emits (mtls_endpoint_aliases,frontchannel_logout_session_supported, the JARM, ID-token and UserInfo signing and encryption algorithm lists;TestDiscoveryDocumented); the authentication flow rulemethodsacceptcertificate; HOTP removal documents409 last_sign_in_method; the descriptions of deletion status, the organization deletion preview (legal holds on members,owned_clients), identity-provider replacement,Idempotency-Key, custom-domain verification conflicts, the single-use upstream-logout return state, dynamic registration updates, client reactivation, organization template conflicts, the organization switch, the token response'stoken_typeand the introspectionagentmember match the server.Migration adapters (MIG-018, MIG-012):
identity-cli migrate-from --applyalways ends withGET /user-imports/reconciliationcarrying the converted export'sexpected_users,expected_memberships,expected_password_verifiersandexpected_totp_factors(records the server skips as deleted left out), also after a run that stopped part-way or had no records; a mismatch (import not reconciled with the converted export: …) or a failed reconciliation request makes the command exit non-zero after writing the report (import.expected,import.reconciliation). Before, it reconciled only when records were submitted, without expected counts (alwaysreconciled: true), and ignored failures.--batch-sizeoutside 1–1000 is refused instead of silently becoming 500. The ZITADEL export command reported byGET /migration-adaptersand--listincludes the"timeout"member of ZITADEL's export request.--source-version(andsource_versionofPOST /migration-plans) accepts a leading part of the version the export states, such as the major version (26for26.8.0), and the refusal names what is expected; unsupported majors are still refused (TestApplyReconciles,TestSourceVersionForms,TestMigrateFromApplyReconciles,TestMigrateFromSuperTokens; migration-adapters.md).Plugins (EXT-014): a workflow a new plugin version drops (or an uninstall keeps as evidence) is retired and no longer re-activated by the next installation or enable; hooks and subscriptions switched off by an emergency control stay off through plugin installs, rollbacks and enables (migration 0084); a hook the package declares
disabledstays disabled; enabling a plugin re-checks its signature, publisher key, contracts and the administrator's permissions; installing a version below an enabled dependent'smin_versionis refused (409);webhook.createdandworkflow.revision.stagedcarryplugin_idfor plugin parts.Workflows (EXT-015/016): retry backoff never exceeds six hours (jitter included); an
assign_rolestep'sreasonis recorded on the assignment; a manual run'sIdempotency-Keyis stored only as a digest; thecall_webhookbody is registered as its own contract (workflow_step_callv1); worker processes use a unique lease owner per process (host name, process ID and a random suffix), so two workers on one host never share leases.Partner project access (TEN-020): project grants end at
valid_untilwith every role assigned through them (project.grant.expired); revoking or expiring a grant moves the holders' authorization revision.Shared signals (AUD-016, ADV-007): a receiver echoing an administrator-set
subject_scope: allonPUT/PATCHis accepted; delivered pushes are no longer relabelledexpiredby retention (and are deleted with other finished events); while the realm's transmitter is off, queued pushes wait without using up attempts; the push endpoint, the check-session origin check and the registry token endpoint are rate limited (the registry also throttles repeated failed sign-ins per address); account erasure removes stream subjects and queued events about the account and redacts the account's federation transfer entries, whose detail is pruned 180 days after a transfer finishes.Refresh rotation keeps each family's own idle window, so offline and remember-me families no longer expire after
sessions.idle_timeoutof inactivity (migration0009; ADR 0011).Audit retention prunes only sealed batches and records them in
audit_prunes, so verification no longer reports pruned batches as digest mismatches (migration0011,TestAuditSignaturesAndRetention; retention now removes individual expired events, see Changed).Restore reconciliation now also re-applies password changes, client-secret and API-key/personal-token revocations, and membership removal, suspension and expiry (backup-restore.md).
proudauth_outbox_oldest_pending_seconds,proudauth_clock_skew_millisecondsandproudauth_database_upare refreshed on every scrape.Support impersonation: disabling support access (deployment, realm, resource, organization or customer opt-out) ends live grants on their next introspection; denied requests and rejected handoffs are audited after the failed transaction rolls back; the decision API denies the same protected actions as
SupportGuard(payments, refunds, ownership transfer, secret reads, deletes) in every support mode (impersonation threat model).Two-party approval for roles marked
requires_approvaluses the authenticated approver instead of a caller-assertedapprover_id(AZ-018).Credential expiry alerts are sent exactly once per credential. The hourly
alerts.credentials_expiringtask used to repeat them; each alert is now claimed by inserting acredential_expiry_alertsrow in the same transaction that enqueues its notifications, so concurrent workers and retried runs cannot notify twice, and a failed enqueue releases the claim. The migration backfills claims for alerts already sent (migration0023, MCH-009, TOK-021, APP-014,TestCredentialExpiryAlerts).Browser login UIs can register: the
pa_txcookie is scoped to the transaction's own path (AUT-016), so it never reachedPOST /registrations. Registration now also has the transaction-scoped routePOST /api/v1/realms/{realm}/auth/transactions/{id}/registration(same body withouttransaction_id) (TestBrowserRegistration; headless-authentication.md, ADR 0017).Account deletion also deletes the user's pairwise subject mappings (
pairwise_subjects) and notification preferences. Theusersrow is kept as a tombstone, so these rows never cascaded (PRV-005,TestPairwiseSubjects; data-inventory.md).Webhook subscriptions created or updated without
allow_private_network: trueare refused private destinations at registration (422ondestination_url) instead of being accepted and then failing every delivery withegress_denied(EXT-007,TestWebhooksAndBackchannelLogout).PATCH /webhooks/{id}is a partial update (EXT-001): members the body omits keep their values (an update no longer resetsallow_private_networktofalseorpayload_formtominimal), a change ofstatealone does not re-resolve the destination, so pausing works while the receiver's DNS fails, andorganization_idis no longer accepted in the body (400) (webhooks.md).Scheduled assisted recovery (MFA-009): when a recovery is due, the worker re-checks that the account still exists and that the requester still has the authority the request route requires (
support.recovery.assistorrealm.users.credentials.manage, and the latter forreset_mfa); otherwise the request is cancelled with reason codeaccount_unavailableorrequester_no_longer_authorized. Requests cancelled this way are not counted as executed. Each due recovery runs in its own transaction, so one failure no longer holds up the rest of the batch (account-recovery.md).Account deletion ends pending work about the account: scheduled assisted recoveries and pending account merges are cancelled, and a pending registration approval is closed as
expired(PRV-005, data-inventory.md).Account merges (USR-011): proposals past their 7 days are marked
expiredby the hourly worker taskaccount_merges.expiryand when a new proposal for the same source account is made, so a lapsed proposal no longer blocks a new one. An administrator can withdraw a pending merge withPOST /account-merges/{id}/cancel(realm.users.write; audituser.merge.cancelled) and either owner can refuse it withPOST /me/account-merges/{id}/decline(audituser.merge.declined). Guest accounts cannot be merged, and execution re-checks that both accounts are still active, registered (not guests) and not protected (TestAccountMerge).delivery.email.allowed_sender_domainsentries are trimmed, lowercased and stripped of a trailing dot before they are compared with a realm sender address's domain, soExample.COMin the configuration matches (MSG-005).Credential-expiry notices name the realm by its display name (or its name) instead of its internal ID, and the
alerts.credentials_expiringtask counts only notices whose transaction committed (TOK-021, MCH-009).Configuration import (APP-019): merging a document's
policyno longer modifies the server's cached copy of the realm policy; the merge works on copies. Lists in the document replace the current list whole, so list elements (for exampleprofile.attributes) no longer inherit members from the elements they replace, and a change that only touches a list is reported as anupdateof the realm policy instead ofunchanged(exit-and-export.md).Configuration export (APP-019) now writes each scope's
required_permissions,step_up_acrandmax_auth_age_secondsand each resource'ssigning_algorithm, so importing an export no longer drops them; clients' newconsent_terms_versionis exported as well.Emergency controls (SEC-020): every audit event recorded while a control runs, including the
client.*,organization.*andservice_principal.*state changes, is filed in theemergencycategory with the operator's reason (when the event has no reason of its own), so it is kept pastprivacy.audit_retentionand raisesemergency_action. Previously only the grant-type, identity-provider, hook and webhook controls were, and the service-principal control recorded no reason (TestEmergencyControls; incidents.md).Maintenance mode
restricted(OPS-015) now letsPOSTrequests whose path ends in/revoke-sessions,/suspendor/lockthrough (for examplePOST /users/{user}/revoke-sessions, user, client and organization suspension, and user locking), so access can still be taken away while configuration is frozen (incidents.md).identity-cli maintenanceandidentity-cli debug-windowwrite the change and its audit event (operations.maintenance.changed,operations.debug_window.opened/.closed) in one transaction, so neither can be recorded without the other;operations.maintenance.changedalso records the public message (public_message).identity-cli config-diff(DEP-020) now also marksfederation.ca_file,federation.egress_allowlist,delivery.egress_allowlist,delivery.sms.webhook_secretandobservability.metrics_enabledas moving a security boundary (configuration.md).PATCH /webhooks/{id}(EXT-001, API-010): the version check is part of theUPDATE, so of two concurrent updates of one subscription the second fails with412 precondition_failedinstead of overwriting the first, with or withoutIf-Match(webhooks.md).External IDs (USR-017): an ID still mapped to a deleted account can be assigned to another account (the old mapping is removed), and an executed account merge moves the merged-away account's external IDs to the surviving account. When both accounts had an ID in the same source system, the survivor keeps both (the source system held two records for one person) and either resolves to it; setting a new ID in that source replaces both. No regression test covers these cases yet (exit-and-export.md).
The
lifecycle.usersworker task (USR-015) continues with the other realms when one realm's run fails and reports the first error afterwards.api/schemas/config.schema.jsonnow documents every deployment configuration key, and the contract testTestConfigSchemaMatchesStructs(tests/contract/config_schema_test.go) fails when a key the server accepts is missing from it or a documented key is rejected (DEP-020).Realm policy
tokens.impersonation_access_token_ttl_secondswas accepted but not applied. It now bounds support grants, together withtokens.access_token_ttl_seconds,impersonation.max_ttland the realm'simpersonation.max_ttl_seconds(the smallest wins); support tokens live as long as their grant (TOK-018; tokens.md). No regression test covers it yet.Consent pre-authorization (APP-013): when a pre-authorization covers a request but the user's own live consent no longer does (other terms version or fewer scopes), the stale consent is revoked and replaced by a
preauthorizedconsent, so revoking the pre-authorization also ends the new grant. A live consent that still covers the request stays the basis. No regression test covers the stale-consent case yet.Decision audit (AZ-020): refusals on organization routes, including those concealed as
404(markedmetadata.concealed: true), and refusals decided inside handlers are recorded asauthorization.denied, once per request, like refusals of realm administration routes.TestDecisionAuditSamplingcovers the realm administration case only (ADR 0013).Localized notices (MSG-006): join-request notices to reviewers and assignees and the decision notice to the requester are written in each recipient's locale and time zone, and the time in the scheduled assisted-recovery notice is rendered in the account owner's time zone (UTC when unknown).
audit_events.recorded_atis now the database's time (now()in the recording transaction) instead of the writing process's clock, so a server whose clock is skewed no longer shifts when its events are sealed (sealing takes events recorded more than 2 seconds ago, by the database clock) or the reportedaudit_sealingbacklog (AUD-014).Audit archive after a database restore (AUD-008):
audit.archiveno longer stops at the first new batch whose sequence number the archive already holds with other content; it continues in a fork directory and recordsaudit.archive.forked(migration0039). The manual step of moving archived files aside after a restore is gone (backup-restore.md).identity-cli verify-audit-archive(AUD-008) now reports missing sequence numbers between files, a first batch that does not start the chain, a file with fewer events than its header records unless it is one of the leading batches pruned before the archive existed (marked witharchived_events), and a fork that does not link to the main archive; it also reportsfirst_sequence,last_sequenceandforks. Previously every short file counted as pruned and the first file's link was not checked (TestAuditArchive; incidents.md).Dynamic groups (TEN-014): each member a rule adds or removes is now audited (
group.member.added/.removed,source: "dynamic"), and so is each manual member removed when a rule is set (source: "dynamic_rule_set").Organization deletion (TEN-011) releases the organization's aliases when it completes, like its slug, so other organizations can take those names (identifiers.md).
An organization's own SMTP server (TEN-022) is reached through each resolved address in turn, not only the first one.
The help text of
proudauth_jobs_failed_totalnow says that it counts dead-lettered jobs and is updated byidentity-worker.Transaction retries no longer repeat pre-action hook calls (DB-014): the token endpoint and authentication steps run their transaction once (
storage.NoRetry) when the realm has an active hook, instead of retrying it on a deadlock or serialization failure, so claims, registration-eligibility and profile hooks are never called twice for one request; other transactions are still retried up to 4 attempts, and tokens are returned only after their transaction commits (TestTransactionRetries; ADR 0004).Imported audit history (MIG-016, PRV-004): records imported for an account that is already deleted are stored without their source-supplied details; restore reconciliation clears those details again for accounts deleted after the backup; the personal data export includes the imported records about the user (
imported_history, at most 500); and imported metadata passes through audit redaction (TestMigrationPlanningAndReconciliationcovers the import-time redaction; the restore and export paths have no regression test yet).Transaction retries (DB-014): answering a flow challenge with a provider response runs once (no retry), and device-code and token-exchange transactions reset their result on each attempt. No regression test covers the reset yet.
Privileged-role detection for
role.assignedmetadata and theprivileged_grantalert follows composed roles at any depth; the reconciliation report still checks one level.Organization templates (TEN-017): a service principal is found by its owning organization after a slug rename instead of being created again; an existing webhook subscription with another destination or event types is reported as a conflict instead of unchanged;
service_principal.createdcarries the owning organization. No regression test covers these yet.The error for an unknown
privacy.audit_retention_by_categoryname no longer listsemergencyas allowed.api/schemas/user-import.schema.jsondocumentsformat,verification_reasonandimport_totp, and thesource_systempattern; the OpenAPI document marksIf-Matchrequired onPUT /hooks/{id}andPUT /organizations/{org}/identity-providers/{id}; the SDK fixture test rejects unknown expected categories.Failing and dead-lettered jobs are recorded again: the update that stores the failure used inconsistent parameter types and failed in SQL, so such jobs could not reach
failedordead_letter.Polling
GET /auth/transactions/{id}stores a re-evaluated flow decision, so risk hooks are no longer called on every poll; a challenge answered in the transaction stays answered when the organization changes (AUT-012).The
503for an unreachable challenge provider suggests the emailed code only when that option is offered to the account (SEC-014).Registering a passkey during sign-in also completes the
configure_totprequired action (MFA-003).login_hint,ui_localesand the user agent are shortened on UTF-8 boundaries (invalid sequences removed) before they are stored, so such input no longer makes a transaction start fail with500.Audit evidence (MFA-012, AZ-020, IMP-006, IMP-024, INV-016): a failed self-service TOTP activation proof is committed as
mfa.totp.proof_failed; passkey enrollment start is audited asmfa.webauthn.enrollment_started(ceremony_id,transaction_id);authorization.decisiondenials carry the subject, the support operator (metadata.actor) andimpersonation_grant_idonce the subject token is resolved;support.token.rejectednames the operator as actor and the customer as subject, with the support grant, organization and route (TestSupportImpersonation); the audit filterimpersonation_grant_idalso returns the request-phase events, andsupport.impersonation.startedcarriesrequest_id(TestCustomerApprovedSupportAccess);invitation.creatednames the invited account as subject (foruser_idinvitations, or the account holding the verified address) and carriesrecipient_digestbeside the masked address: an HMAC of the canonical address under the deployment's lookup key, withrecipient_digest_key_version(INV-016; see Security fixes; data-inventory.md).The Node.js administration client's
inviteBatchkeeps its generatedIdempotency-Keywhen the caller passesidempotencyKey: undefined.identity-cliwarns on standard error when credentials refreshed during a command cannot be saved, instead of ignoring the error; the next command may needidentity-cli loginbecause the stored refresh token can already be spent.The release workflow writes
SHA256SUMSonce, after the image digest and image SBOM exist, and attests the release files only then; it used to attestSHA256SUMSand append the image entries afterwards, so the published file did not match its attestation..dockerignorenow excludes the secret-bearing files.gitignorelists (benchmark seeds,realm-config.json,secrets-out*,*.secrets.json,client-secrets.json,support-bundle.json,revocation-log*,deploy/compose/secrets) in every directory, plus private keys (*.pem,*.key,*.p12,*.pfx),.env*, credentials files and build output, soCOPY . .cannot place them in the build stage or its cache.UserInfo now enforces DPoP-bound tokens (the
DPoPscheme and a matching proof), and device-code and token-exchange grants bind their tokens to the request's DPoP proof or client certificate (TOK-019).Support bundles and
config-diffredact literal challenge-provider secrets (and policy engine tokens) (TestRedactedHidesProviderSecrets).Registering keys (
POST /clients/{client}/credentialswithjwks) for an unknown client answers404.Session-event stream: the
410 cursor_expiredproblem explains how to resynchronize (read the retained history from the beginning, without a cursor); the Go and Node.js bounded-state helpers read the user fromdata.subject_id(user subjects only) forsecurity.refresh_reuse_detected(revoking the user) andauthorization.role.unassigned(revoking the user's membership in that organization), and GoSyncclampswaitto 25 s.Review boundaries (GOV-004):
.github/CODEOWNERSdid not require a security reviewer for client authentication and redirect URI validation (internal/application/), API bearer authentication (internal/httpapi/auth.go), client certificates behind a proxy (internal/httpapi/clientcert.go), or the release inputs the release workflow runs or builds from (Makefileandscripts/offline-bundle.shthroughmake bundle, and.dockerignore); CONTRIBUTING.md only asked for one. They are now security-reviewer paths (TestCodeOwnersReviewBoundaries).
Security fixes#
These were found and fixed before any version was tagged; no released version is affected (only untagged development commits).
Offline-grant access tokens stayed active after "log out everywhere" (SES-005): the revocation revoked every grant and wrote a user-level
token_revocationsrecord that nothing read, so an offline grant's JWT access tokens (which carry nosid) and its UserInfo tokens stayed active at introspection and UserInfo until they expired (about 5 minutes). Introspection (JWT and opaque) and UserInfo now treat a user token without a session as inactive when an account-wide revocation took effect at or after its issuance (one indexed lookup); session-bound tokens, the only user tokens the IdP's own APIs accept, are covered exactly by the session check. Revocations that keep offline grants (account merge survivor, shared signals without offline scope) no longer write the user-level record (TestLogoutEverywhereEndsOfflineTokens; revocation-exposure.md).A sign-in already at the upstream finished through a disabled connection (SEC-020, FED-001, OPS-016): disabling an identity provider (the emergency control, or
"state": "disabled") refused new sign-ins and links, but a round trip started before it could still come back through the callback (OIDC) or the ACS (SAML) within the 10-minute request lifetime, sign the user in or create a just-in-time account or link. Such a round trip now ends withconnection_disabledwithout contacting the upstream again (TestRunbookConnectorCompromise; incidents.md, headless-authentication.md).SCIM write overwrote a concurrent administrator change (TEST-050, PROV-008). A SCIM
PUTorPATCHlocked only the connector's mapping row and read the account (or group) without a lock; after theIf-Matchcomparison it wrote every managed attribute unconditionally, so an administrator'sPATCHthat committed in between was silently reverted althoughIf-Matchhad passed, and a SCIM write withoutIf-Matchreverted it too. SCIMPUT,PATCHandDELETEnow lock the account and its membership (users) or the group row before computing the entity tag, and removing a group member takes a share lock on the group, so the competing change either makes the SCIM write fail with412or is applied on top of it. The conflict policy, including the optionalIf-MatchofPATCH /users/{user}, is documented (TestSCIMAdminRaceNeverLosesChanges; scim-provisioning.md).Identifier probes through the SMS budget and the password lock (API-009). Once
delivery.sms.daily_budgetwas spent, a sign-in code for a number with an account got429while one without an account got200; and the temporary password lock, keyed by account for known identifiers and by identifier otherwise, answered429through every identifier of an account locked through one of them, revealing that they share it. The SMS ceilings are now checked before the number is looked up, and password failures count per identifier and per account: the account's budget still stops all password checks for the account, but only an identifier's own budget answers429, so another identifier of a locked account answers like one without an account. The existing test now asserts that an unknown identifier's lock is identical to a known one's (status, problem code,Retry-After) (TestIdentifierProbeResponses,TestPasswordlessAndAbuseBudgets; configuration.md).Delegation chain dropped from audit (AUD-005).
token.exchangedrecorded the client as actor even when it acts as a service principal or agent and kept only the chain depth, andauthorization.decisionkept only the outermostact.sub(none at all for JWT access tokens, whoseactclaim was not read).token.exchangednow names the service principal as actor and both events keep the wholeactchain (metadata.actor_chain, bounded, agents marked); decisions about an agent's own token carrysubject_agent(TestExchangeActorPreservation,TestActorAuditChain; oauth-oidc-profile.md).Passwordless codes accepted as proof for account linking without saying so (PWDLESS-008). Linking an external identity to an existing account accepted any email code, magic link or SMS code completed in the transaction, including one requested for a plain sign-in before the link was pending, and the message called it a sign-in code. While a link is pending, emailed codes and magic links are now sent with the templates
account_link_codeandaccount_link_magic_link, which name the connection and the linking purpose, and are bound to that link; earlier sign-in codes and links are refused, a linking code is no plain sign-in, and SMS never proves control for linking (TestPasswordlessLinkPurpose,TestLinkProofRequiresLinkBoundEmailProof,TestAccountLinkTemplatesStatePurpose; headless-authentication.md).Restore resurrected consumed proofs (TEST-060). Restoring a backup brought back, as unused, refresh tokens rotated or revoked after it, revoked grants, consumed or replaced recovery codes, used HOTP codes (lower counter), used-up or revoked initial access tokens, rotated registration access tokens and consumed reset, verification and setup codes, and nothing kept the restored database out of traffic before reconciliation. The revocation log now carries
refresh_rotated,refresh_family_revoked,grant_revoked,recovery_code_consumed,recovery_codes_replaced,hotp_counter,initial_access_token(revocations reconstructed from the audit evidence) andregistration_token_rotatedrecords, reconciliation re-applies them and ends every outstanding one-time proof of the restore, and the restore marker keeps the database unready until then (TestRestoreProofConsumption; backup-restore.md).Import role-ceiling bypass (MIG-005). User imports could map members to organization roles above the requester's own authority. Each mapped role must now pass the requester's delegation ceiling in that organization, the worker re-checks at run time that the requester still holds
realm.users.import(and the verification override when email verification is trusted), and roles created by configuration import go through the same ceiling check as the API (TestImportAuthorityAndHashBounds).Unbounded imported Argon2id parameters (MIG-006). An imported verifier could demand arbitrary memory and time at the next login. Verifiers are now capped (memory 1 GiB, 16 iterations, 16 lanes, bounded salt and hash) and hashes with a pepper version are refused (
TestValidateImportedHash,TestImportAuthorityAndHashBounds).Webhook private-network enforcement (EXT-007). The operator's private egress allowance applied to every webhook subscription. Only subscriptions created with
allow_private_network: true(refused unless the operator policy permits private destinations) may now reach private addresses; all others are held to public addresses at delivery (TestWebhooksAndBackchannelLogout).Token-endpoint rate limit failed open (SEC-006). When the rate limiter could not be reached, token requests proceeded unbudgeted; they are now refused with
503 temporarily_unavailable(TestTokenEndpointLimiterFailsClosed).Caller-asserted assurance in
/authorization/check(AZ-016).subject_attributes(assumedacr, authentication age, client, organization) could satisfy MFA or freshness conditions in a real decision about a named subject. They are now accepted only by/authorization/simulate; a real check that supplies them is denied (SUBJECT_ATTRIBUTES_SIMULATION_ONLY,TestPoliciesAndConditionalGrants).Static claim mappings could replace verified user claims (OIDC-007). A client's
id_token_claimscould set OIDC standard user claims such asemail_verified. Such mappings are now rejected at registration (422) and ignored at issuance for clients registered earlier (TestClaimsRequestAndStaticClaims).Generated client secrets stored for idempotent replay (API-006). A configuration import sent with an
Idempotency-Keyheader had its response, including generatedclient_secrets, stored inidempotency_recordsand replayed for 24 hours.client_secretsis now one of the secret markers ininternal/httpapi/idempotency.go, so import responses (which always carry that member) are not stored, and a retry with the same key returns409withoriginal_statusinstead of the secrets (TestConfigImportSecretsNotReplayed).Guest upgrades bypassed registration controls (REG-010, REG-001, REG-008).
POST /me/upgradeturned a guest into a registered account in realms whose registration was closed, invitation-only or limited to other domains. An upgrade is now a registration: the realm and organization registration modes and the registration-eligibility hooks apply (a refused upgrade leaves the account a guest), and in realms withregistration.require_approvalthe upgraded account is queued for review and its sessions end once the address is verified (TestGuestIdentities; headless-authentication.md).Federated just-in-time accounts skipped the approval queue (REG-008, REG-009). Accounts created by a trusted connection are now queued in realms with
registration.require_approvaland cannot sign in until approved (TestFederation).Factor-reset approval without credential authority (MFA-009). Approving a scheduled recovery with
reset_mfaneeded onlysupport.recovery.assist; it now also needsrealm.users.credentials.manage, the permission required to request such a reset (TestScheduledAssistedRecovery).Literal email-provider callback secret (DEP-006, OPS-010). A literal
delivery.email.events_secretwas accepted in production and copied unredacted into support bundles. Production now requires a secret reference for it and fordelivery.sms.webhook_secretwhenever they are set, andConfig.Redactedreplaces a literalevents_secretwith[REDACTED](TestCallbackSecretsNeedReferences).Scope step-up bypass (AUT-010). Token exchange could mint a scope with a step-up policy from a subject token whose authentication was weaker or older than the scope requires, and API keys and personal access tokens could carry such scopes although they hold no authentication event. Token exchange now applies the step-up policy to the subject token's own
acrandauth_timeand refuses the request withinvalid_scopewhen they fall short; API keys and personal access tokens are refused step-up scopes at creation (403 grant_ceiling_exceeded), and introspection leaves such scopes out of existing ones (a credential left without scopes is inactive) (oauth-oidc-profile.md).TestScopeStepUpcovers the token-exchange path andTestAutomationCredentialStepUpScopesthe API-key and personal-access-token paths.Consent revocation across realms (APP-013).
DELETE /users/{user}/consents/{id}did not check that the consent belonged to the administrator's realm, so an administrator withrealm.users.writewho knew another realm's user and consent IDs could revoke that consent and end the client's grants for that user. Revocation is now limited to the realm in the path (TestConsentRevocationStaysInRealm).Lost updates of organization policy (API-010). Two administrators editing an organization's policy at the same time could silently overwrite each other's change, for example re-enabling a sign-in method the other had just restricted.
PATCH /organizations/{org}now requiresIf-Matchwhen it changespolicy(see Changed).Claim mappings released profile data without consent (APP-011). A client's attribute mappings released names, locale, time zone and claimable profile attributes in ID and access tokens whatever scopes the user had granted, and a claimable attribute that users could edit produced user-controlled claim values. Attribute values now require the
profilescope, and attributes that are bothclaimableanduser_editableare refused and never released (TestClaimMappings; see Changed).If-Matchwith version 0 skipped required preconditions (API-010). A header such asW/"v0"was accepted as "no precondition" on routes that require one, so concurrent updates (for example of organization policy) could still overwrite each other. Version0, negative and unparsable values are now refused with412 precondition_failedon every route (TestAdminSDKInterop).Decision cache keys ignored token evidence and group grants (AZ-019). A caller caching
/authorization/checkresults bycache_keycould reuse an allow for a token with another audience or lower assurance, or after a group's roles or a service principal's roles changed. The key now covers the token's kind, audience andacr, the applicable direct and group assignments with role versions, and organization and membership state; decisions are not cacheable for conditional, scheduled or soon-expiring assignments or rule-derived memberships; and the TTL is at most 10 minutes (TestDecisionCacheContract).Token exchange ignored the organization's scope policy (TOK-008). Exchanged tokens could carry scopes outside the organization's
allowed_scopesor whoserequired_permissionsthe subject lacked, and an exchange whose ceiling was empty issued a token without scopes. Both are now refused withinvalid_scope(TestScopeStepUpcovers the organization ceiling,TestExchangeScopeRequiredPermissionstherequired_permissions).Claim mappings could put static or contact values into standard claim names of access tokens (OIDC-007).
access_token_claimsnow refuses every OIDC standard user claim name, static value and attribute mapping alike, includingemail*,phone_number*andaddress(422; see Changed) (TestAccessTokenClaimsRefuseStandardClaims). Issuance skips such names in mappings registered earlier (see Standard claims in access tokens below).Organization templates and mail-server removal without MFA (MFA-003). Applying an organization template (which can create webhook subscriptions and return their secrets) and removing an organization's mail server were possible from single-factor sessions where the realm waives MFA for organization administration. Both now need an MFA session (
TestOrgTemplateAndMailServerRemovalNeedMFA).IPv6 transition addresses bypassed the egress block list (EXT-007). 6to4 (
2002::/16), Teredo (2001::/32) and local-use NAT64 (64:ff9b:1::/48) addresses, which embed IPv4 addresses, were not blocked for webhooks, hooks and other outbound calls; they are now (TestTransitionAddressesBlocked).Secrets in audit metadata (AUD-003). Audit redaction now also covers members named like
api_key,apikeyandaccess_key, in local evidence and in imported audit records (TestMigrationPlanningAndReconciliationcoversapi_keyin imported records).Authentication flow rules could be weakened or answered by the attacker (AUT-012, AUT-014). A
require_mfarule replaced an active phishing-resistant requirement with plain MFA; it now keeps it, so only a user-verified passkey satisfies the transaction (TestApplyFlowKeepsStrongerRequirement). Achallengecould be answered by a factor enrolled in the same transaction, so whoever held only the password could enroll their own TOTP and pass; the challenge is now answered before any in-transaction enrollment, by the provider, an emailed code sent only to the verified primary address, or an already enrolled passkey or TOTP, never a remembered device, and with none available the transaction stops withauthentication_policy_denied(TestFlowChallengeNotAnsweredByEnrollment). When oneauthentication.riskhook failed, the levels of the others were discarded; every hook is now called and their highest level counts as a lower bound (risk: at_least_<level>) (TestRiskHookFailureKeepsOtherLevels).Recovery-restricted sessions could change the account (MFA-010). A session that signed in with a recovery code could change the password and identifiers, request deletion, export data and create personal tokens; it is now limited as described under Changed (
TestMultiFactorAndProofs).A second factor could be added without proving the first (MFA-006). A password-only session (or one skipping MFA through a remembered device) could enroll TOTP or a passkey on an account that already had one (
TestFactorEnrollmentNeedsExistingFactor,TestPasskeys).Transient sessions minted offline refresh tokens (SES-020). With
offline_accessrequested and consented, a transient session produced an offline grant and a refresh token that outlived it (TestOfflineConsentAndTransientSessions).Group membership and moves bypassed delegation ceilings (TEN-008, TEN-013). Roles inherited from ancestor groups were not checked when a member was added, and moving a group under a parent with more powerful roles was not checked at all (
TestDelegationCeilings).Incompatible roles combined through groups and composites (AZ-018). Separation of duties compared direct assignments only; it now covers group and ancestor-group roles and composites, on assignment, group grants, group membership and group moves (
TestSeparationOfDuties).A domain verified by two organizations (TEN-009). Verifying a domain another organization already held verified was accepted; it now ends in
conflict(TestDomainReverification).Another organization's SSO connection (TEN-007, FED-006). An organization could name another organization's connection as its
sso_provider_id;PATCH /organizations/{org}now refuses it (422) (TestFederation), andPOST /organizationsrefuses it too (TestOrganizationCreationRefusesForeignConnection).Organization switch ignored the target's sign-in rules (TEN-005, TEN-012). A session could switch into an organization that requires SSO, restricts sign-in methods or limits session age without meeting those rules (
TestOrganizationSwitch).Token exchange beyond the user's consent (TOK-008). For a subject token issued to a client with
consent_mode: required, exchanged scopes are now limited to the user's live consent for that client (invalid_scopewhen a requested scope is outside it; the default ceiling is narrowed) (TestExchangeLimitedToUserConsent;TestConsentManagementcovers an explicit scope outside the consent).Profile attribute visibility was not enforced (USR-002, USR-007). Self-service responses and exports are now limited to
selfattributes and administrator views never showinternalones; see Changed (TestProfileAttributeVisibility).Jobs ran after their requester lost the permission (INT-006). Audit and configuration exports, user imports and bulk user operations now re-check the requester's permission when they run (dead-letter: "the requester no longer holds <permission>"), and
POST /jobs/{id}/downloadsre-checks it (403) (TestJobAuthorityRechecked).Support writes through the decision API (IMP-011, IMP-013). The server's classification of support actions differed from the SDKs', and an
allowlisted_writesupport token could perform any write through/authorization/check; the server now uses the SDK rule (an action with a write-verb segment is a write whatever its suffix) and the resource'swrite_actionsallowlist (TestSupportClassificationMatchesSDKTablecovers the classification,TestSupportWritesNeedAllowlistthe allowlist).Refresh and ID tokens stored for idempotent replay (API-006). Responses containing
refresh_tokenorid_tokenare no longer stored inidempotency_records; a retry with the same key gets409withoriginal_status. No route returns them through the idempotency layer today, soTestIdempotencyNeverStoresIssuedTokensdrives the middleware directly.Redirect chains through the identity provider (APP-004). Clients could register redirect or post-logout URIs on the identity provider's own origin, whose endpoints redirect onwards (
TestRedirectRegistrationRules).Required actions at account creation (AUT-018).
POST /usersacceptedupdate_passwordandconfigure_totpwithoutrealm.users.credentials.manage, and concurrent required-action edits could overwrite each other; see Changed.Configuration import bypassed write permissions (ARC-008).
realm.configuration.importalone could change the realm policy, clients, grants, resources and roles. Applying a document now needs the write permission of each change, and a policy change needs the reviewedexpected_policy_revision(TestConfigImportAuthority).Runtime credentials could delete audit evidence (AUD-007). Any role could set the session variable the audit guard checked and delete events the way retention does. The guard now accepts deletes only from the table owner inside
proudauth_audit_prune(migration0044), which never removes unsealed,emergencyor held events, and withstorage.runtime_rolethe runtime role has noDELETEon the audit tables.Organization connections crossed tenants (FED-006). A session from an organization's own connection could select or switch into another organization. Selecting (
403), switching (POST /me/organization-grants,403) and transactions whose organization context differs (connection_not_allowed) are now refused.Transient sessions reused through SSO got refresh tokens (SES-020). A sign-in reusing a transient session could still obtain an offline grant. Neither the sign-in nor the token endpoint now issues refresh tokens for a transient session's grants.
Pending custom domains blocked other realms (DEP-014). Registering a host reserved it for every realm and told the administrator that another realm had it. Only verified domains reserve hosts now (migration
0043).Clients could force trace export (OPS-003). A sampled
traceparentfrom any client forced span export; the flag is now honored only from trusted proxies.SCIM fail-open and ceiling bypass (PROV-003, TEST-049). The connector rate limit let requests through when the limiter was unavailable (now
503); a connector could add members to a group whose roles, granted by an administrator or inherited from parent groups, exceeded its ceiling (now403); adoption and domain-restricted registration treated subdomains of a verified domain as verified withoutinclude_subdomains.Standard claims in access tokens (OIDC-007). Older client registrations could still release standard user claim names from static or mapped values in access tokens; issuance now skips them.
Own-origin redirect check bypassed by port or custom domain (APP-004). A redirect URI on the identity provider's host with another port, or on one of the realm's verified custom domains, was accepted.
Forged job rows (TEST-063). Job types are re-checked against their requester's current authority when they run, and a personal export only for the requester's own account, so a queue row naming another requester, subject or tenant dead-letters (
TestForgedJobMetadata).Session-event consumers outlived their authorization (TEST-062). A long poll now re-checks the session and
realm.session_events.readabout every 2 seconds and before releasing events (TestStreamOutlivingAuthorization).Guessable invitation recipient digest (INV-016).
recipient_digestininvitation.createdis now an HMAC under the deployment's lookup key, withrecipient_digest_key_version.
Known limitations#
Details and workarounds are in the linked documents.
- Support impersonation: no intersection with the target's current permissions at issuance; realm state is not re-checked for live grants; there is no bulk revocation (impersonation threat model).
- Restore reconciliation does not cover every change (for example a password marked compromised without a new one, MFA changes, role unassignment or organization suspension) (backup-restore.md).
- Sender-constrained tokens: DPoP and certificate bindings are enforced
at the token endpoint, by the IdP's own APIs and UserInfo, and by the Go
resource SDK's request-level
SenderVerifier. All SDK token-only methods reject bound tokens; Node.js, Dart and PHP have no request-level proof verifier. Refresh tokens are not sender-bound (oauth-oidc-profile.md). - Privacy: authentication transactions hold the full client IP address and the user agent while they are pending, sessions keep user agents, and active consents, pending invitations, registration approvals, assisted-recovery and merge records, suppressions, terms acceptances and legal holds have no retention job (data-inventory.md).
- Metrics and tracing: worker metrics (including
proudauth_delivery_total,proudauth_audit_integrity_problems,proudauth_jobs_failed_totaland theaudit_failuresignal) are visible only whenworkers.metrics_listenis set; dropped spans are not counted; the OTLP exporter does not use the egress policy (monitoring.md). - Audit archive: files are written to a directory only (mount object
storage or a WORM volume there yourself).
identity-cli verify-audit-archivecannot tell that the newest files, or leading files before the first one, are missing, and checks a fork's first link against the main directory only; comparefirst_sequenceandlast_sequencewith the database (incidents.md). - Organization templates cover the organization policy, custom roles, groups, service principals (without credentials) and webhook subscriptions, not clients or identity providers. A dry run does not validate webhook event types, payload form or destinations.
- Dynamic groups: more dynamic groups than the worker recomputes within
security.dynamic_group_max_staleness(up to 4900 per 5-minute run) lose their roles between recomputations; a rule matching more than 10000 members grants nothing until it is narrowed. - Decision cache: callers may reuse a decision for its TTL (at most 10 minutes) after a change the key does not see; decisions that consulted relationships or an external policy engine are never cacheable. The SDKs do not cache decisions.
- Migration: user imports only create accounts (a record already imported
is never updated) and create them
activeorsuspended(locked source accounts have no equivalent). First-party adapters exist only for Keycloak 26.x, ZITADEL v4.x and SuperTokens core 12.x, each tested with one release; they carry over no passkeys, recovery codes, HOTP, SMS or email factors, identity-provider links, phone numbers, service accounts, clients or secrets, flows, policies or sessions, and Keycloak FIPS-padded PBKDF2 verifiers fail to verify. The mapping template leaves out keys that are not organizations by default (Keycloak groups, the SuperTokenspublictenant); dry runs cannot validate memberships in organizations not yet created; the reconciliation--applyrequests has no expected counts (migration-adapters.md). Imported audit metadata is redacted only by member name and token shape, and imported records held by a legal hold at deletion time are not cleared when the hold is released (migration.md). - Lifecycle workflows: no conditions, loops or expressions; subjects are
users only; missed schedule slots are not replayed; endpoint calls are at
least once (endpoints must deduplicate by
Idempotency-Key);extensions.workflow_publish_approvalapplies only to workflows created while it is on; runs keep their accountable publisher after a re-publication; indirect trigger cycles stop only at depth 3; no workflow-specific metrics; account deletion does not clear workflow runs before retention; two workers with the same host name share lease ownership; the depth limit, the 500-run cap, the per-realm share and the transition cap have no tests; workflows are not in the configuration or realm data export (workflows.md). - Plugins: parts can only be hooks, webhook subscriptions and workflows; plugins and their versions are never deleted and are not exported (plugins.md).
- Extension contracts: only webhook events have a second version; hook
request schemas are closed (
additionalProperties: false), so new request members arrive only with a new hook contract version; the realm data export omitsevent_version,contract_versionandplugin_id(extension-contracts.md). - Configuration import: a reconcile replaces a client's whole
registration from the document (client fields it omits take their
defaults); a client whose type differs makes the reconciling import fail as
a whole; resource scopes, client types, denied permissions and approval
flags are never reconciled; nothing is deleted; a document must fit
limits.max_body_bytes(exit-and-export.md). - Configuration import with
Idempotency-Key: the response is never replayed; a retry with the same key returns409, so check the realm's state before re-sending an import whose response was lost (exit-and-export.md). - Realm data export: sections are read one after another, not from one
snapshot; the whole file is built in the worker's memory;
identity-cli apicannot save the NDJSON download, and there is no import for the format (exit-and-export.md). - Untested paths: the role-assignment approval workflow (except for agents) and refresh families with remember-me or offline idle windows across rotations have no automated tests.
- Federation: the published OIDC profiles cover Keycloak 26.8.0 alongside
repository test doubles; the
googleandmicrosoftadapters are tested only for their configuration rules. Other upstream products are untested (interoperability.md). - Authentication flow policy: network, failed-attempt, privilege and
assurance conditions,
require_phishing_resistantand an unavailable challenge provider have no end-to-end test; adenyleaves the transaction pending without an error redirect to the client (headless-authentication.md). - Residency: the deployment's SMTP relay and the OTLP endpoint are checked only at startup, and Vault, KMS commands, the database, backups and logs are outside the policy (data-inventory.md).
- KMS envelope: keys are unwrapped into process memory, and the KMS
command receives the process environment except
PROUDAUTH_*variables (keys.md). - Database roles:
proudauth_audit_prunedoes not check an event's age, so a stolen runtime credential can still prune sealed, unheld, non-emergencyevents early (recorded like retention); without separate roles the runtime credential can delete audit rows directly; the migration's revoke ofCREATEonpublicfromPUBLICtakes effect only when the migration role owns the schema (installation.md). - MFA floor:
admin_phishing_resistantis enforced at sign-in only in realms with passkeys enabled, and realm creation does not check the floor (configuration.md). - Verified-contact update: an account without a primary email address
cannot complete
verify_contact, and a re-confirmed address keeps its earlierverified_at(headless-authentication.md). - Benchmarks: one laptop's reference numbers are published; they do not validate the proposed targets or provide sizing guarantees. Extended scenarios cover software-passkey flows and paced SCIM user lifecycles; physical authenticator latency, delivery workers and multi-node consistency beyond refresh races are not covered (benchmarks.md).
- SCIM: tested only with requests shaped like Okta's and Azure AD's, not
against real directories; no bulk operations or sorting; a connector that
manages more than 20,000 users or groups can list them only with a single
eqfilter onid,externalId,userNameordisplayName(other lists answer400 tooMany); a change to a connector's mappings reaches a group only at its next write by the connector; aPUTorreplaceof a group's members fails as a whole when the group's roles exceed the connector's ceiling (scim-provisioning.md). - SAML: tested against the repository's own test SP and IdP and the
published Keycloak 26.8.0 profiles (SAML-010); other products and external
conformance suites are untested. Not supported:
Artifact binding, SOAP, attribute queries, ECP, proxying, metadata signing
and metadata URLs, IdP-initiated logout and logout propagation (logout
reports
PartialLogoutinstead), HTTP-Redirect ACS endpoints, encrypted assertions and upstream logout at the broker, any SHA-1 or algorithm exception. One SAML key set per realm serves both roles; recreating a SAML application changes its persistent NameIDs; SAML applications, connections and keys are not part of the configuration document (saml.md). - Directories and outbound provisioning: LDAP is tested only against an
in-process server; outbound SCIM also has a published ZITADEL v4.16.3
profile alongside the in-process double. Other counterparties are untested
(interoperability.md). Not supported:
Kerberos/SPNEGO (PROV-015), password and identifier write-back, SASL and
anonymous binds, nested groups and ranged retrieval, AD DirSync and RFC 4533
synchronization, referral chasing, a lookup-only storage mode, outbound
SCIM bulk, extension schemas and authentication other than a bearer
token. A directory outage blocks password sign-in for its users (by
design); organization connectors reactivate a membership on re-enable
whoever suspended it; account deletion keeps the directory link's DN and
imported values, and runs, findings and deliveries have no retention job;
disabling
directory.enabledwhile a run is queued leaves the runqueuedand blocks new runs of that connector (directory.md, data-inventory.md). - Not implemented: SCIM bulk operations, JAR (signed request objects),
CIBA, OCSP and CRL fetching, user assertions in
jwt-bearer, signing or decryption inside an HSM, Helm chart; embedded or sandboxed code execution is excluded by design (EXT-013, ADR 0021). - The Node.js SDK is tested (in CI) only against local mock HTTP servers.
- OIDC Session Management: browsers that block or partition
third-party cookies report every session as
changed, server-side revocations are invisible to the iframe, and the iframe script has not been run in real browsers (oauth-oidc-profile.md). - Shared signals: no long polling, no polling of remote transmitters, no
CAEP
assurance-level-change,token-claims-changeordevice-compliance-changeevents out; tested only against the repository's test servers. Revoked grants keep a user in aclient_usersstream until their rows are purged, and account deletion leaves the user ID inssf_stream_subjects(shared-signals.md). - Partner project access: a grant's
valid_untilstops new assignments but does not remove or disable existing ones; ending a relationship leaves existing grants in place (partner-access.md). - Passkey trust: the FIDO metadata BLOB is read at startup only and has
not been tested with real metadata; with
server.host_policy: strict, related origins work only when the RP ID is the public origin's host (passkey-trust.md). - Registry token bridge: no registry implementation has been run against it; machine-client and API-key credentials are untested there (container-registry.md).
- Historical entitlements: service principals are always reported
active, OAuth grants older thanprivacy.session_retentionafter they ended are gone, and reconstruction cannot go past audit retention (investigations.md). - Realms cannot be deleted (only suspended), and share the database, root keys and upgrade timing of their deployment (isolation-tiers.md).
- FAPI 2.0: the
fapi2profile is not certified by the OpenID Foundation and no conformance suite has been run (oauth-oidc-profile.md). - Mutual TLS and X.509 sign-in: no OCSP or CRL fetching; an anchor holds
only its own CRL, so under
revocation: crlcertificates issued by an intermediate are refused unless the issuing CA is the anchor; tested only with in-process TLS and generated certificates (mutual-tls.md). - Workload federation: tested only with locally signed tokens in the
shape of GitHub Actions, Kubernetes and SPIFFE tokens;
jtireplay control is per trust; there is no emergency control for the grant; trusts are not in configuration documents or the realm data export (workload-federation.md). - Dynamic client registration: no software statements or
jwks_uri; registration access tokens do not expire and the management endpoints have no rate limit; registrant-supplied contacts are not verified (they are never mailed, but count as contacts in the ownership report) (dynamic-client-registration.md). - Relationship-based authorization: links follow one hop; tuples and
shares are not removed with organizations and are not exported;
list-objectsapplies no step-up, policies, engines or hooks (relationship-authorization.md). - Elevation and access reviews: the
statefilter shows an elapsed elevation asactiveuntil the sweep runs; campaigns cannot be wider than 10000 assignments and are closed item by item (elevation-and-access-reviews.md). - Front-channel logout: delivery is not confirmed and third-party cookie restrictions often defeat it; frames are produced only by the logout endpoint, not when a session is revoked elsewhere (oauth-oidc-profile.md).