Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Identity and access management (RFC0037)

IAM combines the existing local account and database-role model with replicated profiles, API keys, optional federated OIDC/OAuth providers, and SCIM-managed identities. The candidate implements D1–D6 of RFC0037, and its bounded source-bound local validation has passed. This chapter describes code behavior, not a release qualification or support claim for any particular IdP; PR CI verification remains.

Edition boundary and migration

Community includes local users, service principals, database roles, direct grants, permission profiles containing Community permissions, and local administration. Professional and Enterprise include the supported fine-grained label, relationship-type, and relationship-property permissions. Enterprise adds OIDC/OAuth federation, external identities, workload tokens, SCIM and provider administration. A profile is gated by the permissions it contains. Reducing access remains available after expiry or downgrade: revoke grants, unassign profiles or bindings, suspend or remove principals, and disable providers. Stored restrictions continue to apply. License checks block only operations that would add or widen access.

Existing local accounts and their direct role/grant semantics remain usable; profile mode is explicit and does not silently replace direct grants. Configure the local root administrator as break-glass access before enabling a provider. Once provider configuration has been used, a replicated sticky enforcement marker prevents deleting the final provider from reopening anonymous admin access. The marker survives reopen, Raft snapshot/install, and administrative restore/import; only actual provider configuration sets it. Do not treat an empty provider list as permission to turn authentication off.

Raft peer protocol 19 introduces IAM commands and snapshot header state; the current snapshot format constant is 6. Every cluster member must support the IAM protocol before IAM state can be restored or used. A portable/offline administrative restore merges IAM records into the target and imports providers disabled and quarantined; an administrator must reconcile and explicitly release them before re-enabling. Local users, service accounts, profile state, external records and tombstones are merged. This is distinct from an exact Raft snapshot install, which installs the snapshot’s replicated state at its cut.

Local principals and profiles

A local interactive user authenticates with a password; a service principal uses API keys. API-key plaintext is shown only at creation, uses the pdbk_<16-hex-id>_<base64url-secret> form, and only its SHA-256 verifier is stored. Up to 16 API keys are accepted per service account. Keys can expire and be individually revoked. A SCIM credential uses the analogous pdbs_... form; only its verifier is stored, plaintext is shown once, and up to two credentials per provider support rotation.

Direct access mode preserves the existing role and per-database grant model. Profile mode composes the assigned flat profiles with closed database defaults; profile permissions do not merge into direct grants. Local principals can be suspended, and account authorization changes are replicated. Management list endpoints use offset and limit, defaulting to 50 and capped at 100; responses include total, offset and limit. SCIM list responses cap a page at 200.

A profile-managed account ignores its direct role and grants. GRANT, REVOKE and ALTER USER … SET ROLE against it, and role/db_roles in a management edit, are refused with 409 and an explanation instead of reporting a change that has no effect; change its profiles, or switch it to direct mode first. An administrator editing their own account’s global role, access mode or (profile mode) profile assignments must send "confirm_self_change": true; the configured root stays the recovery path. User-management statements that contain a password are shown in SHOW TRANSACTIONS / /mgmt/transactions only as their literal-free fingerprint.

Federation and SCIM

OIDC/OAuth access tokens are verified against configured issuers, audiences, allowed JWS algorithms and JWKS keys. Defaults in the model include subject claim sub, token types at+jwt and application/at+jwt, groups claim groups, JIT enabled for token-claims membership, 60 seconds clock skew, one-hour maximum token age, 5-minute to 24-hour JWKS cache bounds, 30-second unknown-key refresh, one-day maximum JWKS staleness, 512 groups and a 16-KiB token limit. These are configurable implementation defaults, not a statement that every provider’s tokens or claims are supported.

The two membership modes are explicit. Token-claims mode can create a principal just in time and uses verified token membership. Its default groups claim is groups. SCIM-managed mode uses SCIM as the authority for account activation and group membership; its default token correlation claim is sub. Token groups do not restore membership or reactivate a disabled account. Deletion/deactivation does not resurrect prior access on re-creation. Pre-JIT tombstones can block first login. Provider records support optional workload-client allowlists and per-provider strict freshness; strict freshness is off by default and applies to external principals only.

A provider with a workload policy is workload-only unless an explicit signed human-token discriminator is configured. For example:

"workload": {
  "client_claim": "azp",
  "allowed_clients": ["etl-client"],
  "human_token": {"claim": "idtyp", "value": "user"}
}

Use the actual claim and value your IdP guarantees for human tokens; the example is not a universal IdP mapping. Omit human_token for workload-only providers. Existing mixed-provider configurations with workload must add this explicit human discriminator before human logins can continue. Every other token must carry a string client claim on the allowlist, regardless of whether its subject equals the client identifier. Missing or malformed client claims are rejected. Providers without workload retain their human-token mode. An external identity is scoped by provider, subject and kind: a workload token never resolves to a principal created for a human token with the same subject, and vice versa. In scim_managed mode only SCIM-provisioned principals can sign in; a principal left over from earlier token-claims JIT cannot. Switching an enabled provider to scim_managed needs the same recent break-glass verification as enabling one.

Tokens carrying the ID-token-only claims at_hash or c_hash, or a typ: "ID" claim, are refused even when a provider accepts a generic JWT header type. Prefer RFC 9068 at+jwt tokens; if an issuer can only send JWT, list it in token_types and give the Patina API its own audience, distinct from the Console’s client ID. exp, nbf and iat accept integer or fractional NumericDates; any other value is malformed. An overage or incomplete group list (hasgroups, _claim_names.groups) grants no group-binding access and logs a warning naming the provider and principal.

Key refreshes are single-flight per provider and run outside the cache lock: a verification whose key is cached and still usable never waits for a slow or unreachable IdP. At most 64 verifications run at once; a request that cannot start within 5 seconds fails as “identity provider unavailable”. These are safety bounds, not measured capacity figures. unknown_kid_refresh_secs has a one-second floor. jku, x5u and embedded jwk headers are ignored and never fetched. IPv6 addresses that embed or translate to IPv4 (mapped, compatible, NAT64 64:ff9b::/96, 6to4 2002::/16, Teredo) are classified as that IPv4 address by the outbound address check.

Failed JWKS fetches, including an empty initial cache, are retried no more often than unknown_kid_refresh_secs (default 30 seconds, minimum one second for failure backoff). Provider revision changes invalidate this retry state. Each request still checks maximum staleness measured from the last successful fetch; backoff never extends that deadline. Recovery after an outage may wait for the retry window.

Outbound IdP calls require HTTPS by default. Private, loopback or link-local network destinations require explicit CIDR allowlisting; cloud metadata endpoints remain blocked. The console has separate explicit switches for allowlisted private networks, custom CA material and development-only HTTP. Do not enable insecure HTTP in production. The console requires a configured public HTTPS origin outside loopback development, and its OIDC callback must match that origin. Without --sessions, all modes are loopback-only and reject --public-origin; shared credentials must not be exposed through a public Console listener.

Bearer tokens on REST and Bolt

REST clients send Authorization: Bearer <token>; an invalid bearer never falls back to Basic or anonymous access. Bolt dispatches the scheme explicitly: basic (or absent), bearer and none; any other value is refused as Neo.ClientError.Security.Unauthorized. For bearer, the caller-supplied principal is ignored. An expired token is Neo.ClientError.Security.TokenExpired on REST (with WWW-Authenticate: Bearer error="invalid_token", error_description="token expired"), on Bolt 5.1+ LOGON, on pre-5.1 HELLO (Bolt 4.4/5.0), and on later messages of a retained session, where an open transaction is rolled back. A failed LOGON leaves the connection open for a fresh LOGON; an unreachable IdP is Neo.TransientError.General.DatabaseUnavailable.

With the official Python driver this means neo4j.bearer_auth(token) for a static token (the driver raises neo4j.exceptions.TokenExpired) and neo4j.auth_management.AuthManagers.bearer(provider) for automatic refresh: the driver asks the provider for a new token after the server reports TokenExpired, and managed transactions retry. Verified locally with driver 6.2.0 and 5.28.2 against Keycloak 26.8.1 (see the recipe below); other drivers are unqualified.

Keycloak recipe

The repository harness tests/iam-keycloak/ applies this recipe to a real Keycloak container and checks it (Keycloak 26.8.1, 2026-10-21; see the RFC implementation status). It qualifies only that version and configuration.

  1. In a realm, create a bearer-only client patina-api (the resource audience) and add these protocol mappers to every client that requests Patina tokens: an Audience mapper adding patina-api to access tokens only; a Group Membership mapper with claim groups and full path off; and, for application roles, a User Client Role mapper for client patina-api with claim patina_roles (multivalued).
  2. Enable Use “at+jwt” as access token header type (client attribute access.token.header.type.rfc9068) on those clients, so tokens are RFC 9068.
  3. For workloads, create a confidential client with service accounts only (client credentials) and put its service-account user in the groups it needs.
  4. For humans, mark tokens with a signed discriminator, for example a Hardcoded claim mapper patina_principal=human on the human-facing clients only (never on a client with service accounts enabled).
  5. For the Console, a confidential client with the standard flow, PKCE S256 and the exact redirect URI https://<console origin>/auth/oidc/callback.
  6. In Patina (PUT /mgmt/iam/providers/keycloak): issuer https://<host>/realms/<realm>, audiences: ["patina-api"], algorithms: ["RS256"], token-claims membership with groups_claim: "groups" and roles_claim: "patina_roles", workload: {"client_claim": "azp", "allowed_clients": [...], "human_token": {"claim": "patina_principal", "value": "human"}}, and outbound.ca_pem/allowed_networks for a private Keycloak. Then bind group names or roles to profiles.

Caveats: Keycloak’s group-membership mapper emits group names, not stable IDs, so renaming a Keycloak group changes the claim; prefer client roles for privileged mappings. Group changes reach Patina only with new tokens (the token-claims boundary); use emergency disable for immediate revocation. Keycloak does not provision outbound SCIM, so this recipe is token-claims mode. Microsoft Entra ID and Okta recipes are not yet written or qualified.

SCIM service provider

/scim/v2/{provider} accepts only that provider’s pdbs_… credential and only while the provider is enabled, unquarantined and scim_managed. It supports:

  • Discovery: ServiceProviderConfig, ResourceTypes, ResourceTypes/{id}, Schemas, Schemas/{id}. Any other path under the base answers a SCIM error (404) behind the same credential.
  • Users and Groups: GET/list, POST, PUT, PATCH, DELETE; startIndex/count (at most 200), ETags and If-Match; attributes and excludedAttributes (top-level names; schemas, id and meta are always returned).
  • Filters: only attr eq "value" on userName/externalId (users) and displayName/externalId (groups); values match case-sensitively. Other filters are 400 invalidFilter. Bulk, sorting and POST /.search are unsupported.
  • User PATCH: add/replace of active, displayName, userName, externalId (same value only), emails, or a pathless object; remove of displayName or emails.
  • Group PATCH: operations apply in order; add/remove/replace of members (a remove without a value removes all members), remove members[value eq "…"], add/replace of displayName and externalId (same value once set).

Identity rules: externalId is immutable once provisioned, for users (the token correlation key) and groups (the binding key); changing it is 400 mutability, so delete and re-create instead. active changes only when an explicit boolean (or the string "true"/"false") is sent; omitting it keeps the current state, and removing it is refused. Deactivation removes membership effect and local profile assignments; reactivation restores only the identity. Delete + re-create yields a new principal without memberships or assignments, and an administrator’s emergency disable carries over to it. Group members must be provisioned users of the same provider: an unknown member is a retryable 409, a member naming a group is a permanent 400 (no nested groups); roles/entitlements never become grants.

Checked locally with the independent scim2-tester 0.5.2 client (tests/iam-scim/): 47 checks pass; the remaining ones are the deviations above (no /.search, a single primary email, active not removable, unknown member references). This is not a qualification of Entra ID or Okta provisioning.

Console sessions and revocation

Console sessions are node-local and in-memory, with defaults of 30-minute idle timeout, 12-hour absolute lifetime, 5-minute recent-authentication window and a 10,000-session cap. One principal holds at most 16 concurrent sessions; its oldest is retired first, and the global cap evicts the oldest-created session, so repeated sign-ins by one account cannot push other operators out (both are safety bounds, not measured figures). They use secure host cookies outside explicit loopback development mode. The console uses per-session upstream credentials; configuring a shared upstream credential is rejected in session mode. During activity, the session idle, absolute and recent-authentication windows can be configured with --session-idle-secs (default 1800), --session-absolute-secs (default 43200) and --session-recent-auth-secs (default 300). Every protected request re-verifies upstream identity and effective access; logout removes the session without a network check. A changed effective-access fingerprint, including database roles and grants, retires the old session ID and requires a fresh login. Password reset, suspension, credential revocation or identity replacement therefore ends a retained session at its next request. This is not a cluster-wide shared browser session store. Recent authentication is required for sensitive IAM UI actions and all admin POST /query requests, including read queries, because the same endpoint can execute user/role DDL. Reauthenticate after the configured recent-authentication window before using the admin query editor. Non-admin queries retain their normal session lifetime.

A server or identity provider that is unreachable or failing is reported as 503 upstream_unavailable, not as invalid credentials. Organizational sign-in failures return to the Console with a signin_error code that the sign-in panel explains (not_authorized, identity_mismatch, upstream_unavailable, oidc_unavailable, invalid_state, …). An IdP-authenticated user without any Patina mapping gets a session whose effective access grants no database. The Identity & Access view shows the configured break-glass root, when it was last verified and by whom, and offers emergency disable for managed principals.

Public sign-in and existing-session verification have separate 32-request network budgets. Existing sessions wait up to 10 seconds for their budget; a capacity timeout returns 503 without retiring the session. No successful authority check is cached: immediate revocation checks still occur per request. OIDC pending state is held in an authenticated encrypted HttpOnly cookie with a 10-minute absolute lifetime, not a server-side pending-login pool. The IdP must enforce single-use authorization codes; PKCE, nonce and state remain mandatory. Restarting the Console invalidates pending logins. Discovery is coalesced and cached for 5 minutes on success and 10 seconds on failure; outbound validation still applies when using the cached endpoints.

The current candidate revalidates authorization before replayed and live /changes events and after resynchronization. Expiry, revocation, or an unavailable check ends the stream with an SSE error and clears queued events. Targeted expiry and revocation regressions passed in the source-bound local validation. This does not promise instantaneous cluster-wide revocation before a change is committed; PR CI verification remains.

The replicated write fence binds authorized writes to principal and policy versions so revocations ordered before a write are enforced during apply on each replica. Strict freshness is a separate opt-in read barrier for external principals; local users retain the normal replicated-apply contract. These mechanisms do not promise instantaneous propagation before a change commits.

Audit and recovery

IAM state is replicated control-plane state, separate from graph records. Audit entries use the existing audit system; tracing/export is not an audit-delivery guarantee. Recovery procedures and credential rotation remain operator tasks. Use Security for authentication fundamentals and Limitations for current qualification boundaries.

Identity restore and crash recovery

Portable REST restore submits the user records and optional IAM metadata in one ordered authorization-fenced identity command after restoring graph data. It can restore the calling local administrator’s own account; a revocation ordered before this command still rejects it. Protocol 19, valid unique user names and the complete identity command’s entry-size limit are checked before graph writes. An identity bundle that exceeds the entry limit is rejected rather than split across authorization changes. Restoring identity state does not make the whole multi-database graph restore atomic.

Legacy backups without IAM metadata preserve existing local principal IDs, suspensions, profiles and service keys. Backups carrying IAM metadata merge it and quarantine providers as before. Exact Raft snapshot installation remains an authoritative replacement, separate from portable backup restore.

An accepted IAM command stores a digest and its log position atomically with its IAM changes. Crash replay of that exact command completes credential side effects even if the command changed its own authorization version. Another command or log position gets no such exemption. Apply progress is persisted at IAM boundaries before another receipt can replace it; normal graph group commit is unchanged.