OpenID Connect
Where PermDock sits in an OpenID Connect deployment: the relying party or resource server runs Discovery and verifies the token, permdock/jwt maps the verified claims to a Subject, and every OIDC claim that reaches the principal has one documented home.
permdock/jwt implements the discovery option, the accept option and the claim-to-Subject mapping below, and permdock/ssf accepts Back-Channel Logout as a revocation input. The provider adapters (supabase, clerk, better-auth) map the claims they already expose. Identity Assurance verified_claims is mapped into principal.assurance.verified; Federation, the Enterprise Extensions and IPSIE stay on the watch list.
What it is
OpenID Connect Core 1.0 is the identity layer on top of OAuth 2.0: an OpenID Provider (OP) authenticates an End-User and returns an ID token, a signed JWT that tells the Relying Party (RP) who was authenticated, when, how, and for which client. The specification is Final (incorporating errata set 2, December 2023) and has been adopted as ISO/IEC 26131:2024 and ITU-T X.1285, which makes it the most widely deployed federated identity protocol and the one every provider PermDock has an adapter for (Auth0, Okta, Entra, Google, Supabase, Clerk, Better Auth) implements. The parts a permissions library cares about:
- The ID token claims (Core section 2).
iss,sub,aud,exp,iat, plusauth_time,nonce,acr,amr,azp.subis "locally unique and never reassigned within the Issuer", so the pairiss+subis the only stable identity;subalone is not. - Subject identifier types (Core section 8).
public(the samesubfor every client) orpairwise(a differentsubper client sector). A permissions layer that keys grants onsubmust know which one it has. - Discovery (OpenID Connect Discovery 1.0).
GET <issuer>/.well-known/openid-configurationreturns the OP's metadata:issuer,jwks_uri,id_token_signing_alg_values_supported,acr_values_supported,claims_supported. RFC 8414 defines the same document for plain OAuth 2.0 authorization servers at/.well-known/oauth-authorization-server. - Access tokens versus ID tokens. OIDC says nothing about the access token's format; RFC 9068 does, as a JWT with
typ: at+jwt, and it is the access token, not the ID token, that a resource server authorises. The ID token's audience is the client; the access token's audience is the resource. - Session lifecycle. Back-Channel Logout 1.0 delivers a
logout_token(a JWT withtyp: logout+jwt, aneventsclaim naminghttp://schemas.openid.net/event/backchannel-logout, andsuband/orsid) to the RP when the OP session ends. Thesidclaim identifies the session the token came from. - Step-up. RFC 9470 lets a resource server answer
WWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="...", max_age=...when the token'sacrorauth_timeis not good enough, and the client re-authenticates with those parameters.
Around Core sit the specifications the watch list tracks: OpenID Federation (trust without pairwise configuration), the Enterprise Extensions (session_expiry), the IPSIE profiles, Identity Assurance (verified_claims), the Ephemeral Subject Identifier draft and Key Binding.
Why it matters for PermDock
PermDock never authenticates. The OIDC flow ends where PermDock starts: once the RP or resource server holds a verified token, permdock/jwt or a provider adapter turns its claims into a Subject and createPermDock decides from there. Three things go wrong when that boundary is fuzzy, and each is a rule below:
- Keying grants on
subalone. Two issuers can both mintsub: "1234". The principal carriesissuernext toid, and audit events record both. - Authorising an ID token. An ID token proves a login happened for a client; it says nothing about what the client may do at an API.
permdock/jwtrejects ID tokens presented as access tokens unless a deployment opts in. - Hard-coding
jwks_uri. Providers rotate keys and occasionally move JWKS endpoints. Discovery is the source ofjwks_uriandissuer, and the issuer in the document must equal the issuer asked for.
How PermDock uses it
Discovery in permdock/jwt
import { createJwtSubjectResolver } from "permdock/jwt";
const resolve = createJwtSubjectResolver({
discovery: "https://login.example.com", // fetches /.well-known/openid-configuration, then RFC 8414
audience: "https://api.example.com",
algorithms: ["ES256", "PS256", "Ed25519"],
claims: {
id: "sub",
roles: "roles",
assurance: { acr: "acr", amr: "amr", authTime: "auth_time" },
},
});discovery replaces jwks and issuer: the resolver fetches the document lazily, requires issuer in the document to equal the configured issuer byte for byte (Discovery section 4.3), takes jwks_uri from it, and caches both under the JWKS cache rules. Fetching happens in the resolver, never in decide (invariant 15). A document served over plain HTTP, with a mismatched issuer, or without jwks_uri is a configuration error reported once by permdock doctor; tokens resolve to anonymous until it is fixed. Full option list on the JWT adapter.
Access tokens by default, ID tokens by opt-in
accept: 'access-token' (the default) requires typ: at+jwt under profile: 'fapi2' and otherwise accepts at+jwt or JWT; a token that looks like an ID token (nonce present, aud equal to a client identifier rather than the configured audience) is rejected with reason wrong-token-type. accept: 'id-token' is for backends-for-frontends that verify the ID token themselves and want its claims as the principal; it requires audience to be the client id and applies Core section 3.1.3.7 validation (aud, azp when several audiences, iss, exp, iat). A resolver accepts one or the other, never both.
Claims that reach the principal
| OIDC claim | PermDock field | Rule |
|---|---|---|
sub | principal.id | Opaque string; compared byte for byte; never a display name or email |
iss | principal.issuer | Always set by permdock/jwt and the provider adapters; audit events carry issuer next to id |
aud, azp | Verified, not stored | aud must contain audience; with accept: 'id-token' and several audiences, azp must equal the client id |
exp, session_expiry | subject.expiresAt | min(exp, session_expiry); copied into the snapshot |
iat, nbf | Verified, not stored | Within clockTolerance |
acr | principal.assurance.acr | The authentication context class reference, compared as an opaque string against the policy's step-up conditions |
amr | principal.assurance.amr | Array of RFC 8176 method names (pwd, otp, hwk, mfa); conditions may require a member |
auth_time | principal.assurance.authTime | Seconds since the epoch; the input to max_age style conditions |
sid | subject.session | Session identifier, used to match a later logout_token or CAEP session-revoked event to the snapshot |
nonce | Rejected on access tokens | Its presence is one of the signals for wrong-token-type |
roles, groups, entitlements | principal.roles, principal.memberships | RFC 9068 claim names and SCIM encoding (JWT authorization claims) |
org_id, tid, hd, org_code | principal.tenant | No standard claim exists; the provider page names the claim; a tenant with no matching membership is no tenant |
verified_claims | principal.assurance.verified | OpenID Connect for Identity Assurance 1.0: one object or an array, normalised to an array of { verification, claims } kept as the issuer sent them and frozen. An entry without verification.trust_framework or a claims object, or with a __proto__, constructor or prototype key anywhere, is dropped. Conditions read it as a ref (principal.assurance.verified['0'].verification.trust_framework); PermDock never interprets the evidence. Path option claims.assurance.verified (default verified_claims) |
email, name, picture | Nowhere | The provider owns them; conditions do not need them (subject) |
user_metadata-style claims that the End-User can edit are never mapped to roles or memberships (authentication).
Step-up denials
A permission whose condition reads subject.assurance (for example acr in a required set, or authTime newer than a bound) is denied with reason insufficient-user-authentication. HTTP adapters render that exact reason as the RFC 9470 challenge, WWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="<required>", max_age=<seconds>, taking acr_values and max_age from the condition that failed; the status is 401 and the Problem Details body is type: .../step-up-required carrying the same values as acrValues and maxAge (Problem Details). MCP returns the same acr_values and max_age in the refusal, and an input_required URL request to stepUp.at when it is set. The mapping from every denial reason to RFC 6750 and RFC 9470 is on the JWT adapter.
Back-Channel Logout as a revocation input
permdock/ssf accepts a logout_token next to Shared Signals SETs: same TokenVerifier, typ: logout+jwt required, events must contain the back-channel logout member, nonce must be absent (Back-Channel Logout section 2.6), sub or sid must be present. A matching snapshot (by principal.id plus issuer, or by session) is invalidated exactly as for CAEP session-revoked (Shared Signals). This is a receiver for a token the OP already sends; PermDock does not implement the RP's logout endpoint routing.
What PermDock does not do
It does not run the authorization code flow, exchange codes, refresh tokens, verify nonce on the RP's behalf, manage the RP session, or implement Federation trust-chain resolution; Federation stays a deployment concern, including for permdock/pdp. discovery takes one issuer: a deployment that trusts several issuers builds one resolver per issuer. Those are the provider SDK's or the OAuth client library's job; PermDock consumes their result. It does not support the Implicit Flow's id_token in a fragment as authorization material.
Mapping table
| OpenID Connect concept | PermDock concept |
|---|---|
| OpenID Provider | The issuer configured through discovery; principal.issuer |
| Relying Party / resource server | The application running createPermDock; permdock/jwt is its token-to-subject step |
| End-User | principal with kind: 'user' |
Client (azp, client_id) | actor when it differs from the subject and acts under a delegation; otherwise verified and dropped |
| ID token | Accepted only with accept: 'id-token'; never as authorization for an API |
Access token (RFC 9068 at+jwt) | The default input to subjectFromJwt |
| Discovery document | Source of jwks_uri and issuer; cached with the JWKS |
acr, amr, auth_time | principal.assurance.{acr, amr, authTime} |
verified_claims | principal.assurance.verified |
| RFC 9470 step-up challenge | denied with reason insufficient-user-authentication, rendered as WWW-Authenticate |
sid | subject.session; the join key for logout and CAEP events |
Back-Channel Logout logout_token | Revocation input to permdock/ssf |
Public vs pairwise sub | Documented on the provider page; grants and audit key on issuer + id either way, with no sector marker on the principal |
| Federation entity statements | Tracking: candidate trust mechanism for permdock/pdp |
Sources
- OpenID Connect Core 1.0 incorporating errata set 2, sections 2, 3.1.3.7 and 8; How OpenID Connect works.
- OpenID Connect Discovery 1.0, sections 3 and 4.3; RFC 8414.
- OpenID Connect Back-Channel Logout 1.0, sections 2.4 to 2.6.
- RFC 9068 (JWT profile for access tokens), RFC 9470 (step-up authentication), RFC 8176 (
amrvalues). - OpenID Foundation specifications index for Federation, the Enterprise Extensions, IPSIE and Identity Assurance.
Related
- JOSE: the token formats and algorithms behind every OIDC artefact.
- JWT adapter:
discovery,accept, the claim options and the error mapping. - Authentication: the verified-material rule and the provider recipes.
- Subject:
principal.issuer,assurance,session. - FAPI 2.0: the high-security profile of the same flow.
- Shared Signals and CAEP: the other revocation input.
- subject: why
issuer,assurance.acrandsessionwere added. - Standards watch list.
Last updated on
Postgres row-level security
Postgres RLS as a compile target and import source for PermDock policies, covering CREATE POLICY semantics, Supabase and Neon helpers, GUC patterns, pg_policies introspection, the Drizzle and Prisma 8 authoring surfaces, testing and the risks of round-tripping.
JOSE (JWT, JWS, JWE, JWK, JWA)
The JSON Object Signing and Encryption family as PermDock consumes it (bearer JWTs, JWKS, cnf bindings) and produces it (JWS-signed snapshots, approval tokens, decision exports), with the RFC 8725 / rfc8725bis checklist permdock/jwt follows and the interoperability contract that lets any language verify what PermDock signs.