PermDock
Standards

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, plus auth_time, nonce, acr, amr, azp. sub is "locally unique and never reassigned within the Issuer", so the pair iss + sub is the only stable identity; sub alone is not.
  • Subject identifier types (Core section 8). public (the same sub for every client) or pairwise (a different sub per client sector). A permissions layer that keys grants on sub must know which one it has.
  • Discovery (OpenID Connect Discovery 1.0). GET <issuer>/.well-known/openid-configuration returns 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 with typ: logout+jwt, an events claim naming http://schemas.openid.net/event/backchannel-logout, and sub and/or sid) to the RP when the OP session ends. The sid claim 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's acr or auth_time is 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:

  1. Keying grants on sub alone. Two issuers can both mint sub: "1234". The principal carries issuer next to id, and audit events record both.
  2. 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/jwt rejects ID tokens presented as access tokens unless a deployment opts in.
  3. Hard-coding jwks_uri. Providers rotate keys and occasionally move JWKS endpoints. Discovery is the source of jwks_uri and issuer, and the issuer in the document must equal the issuer asked for.
ID token, access token /.well-known/openid-configuration jwks_uri, issuer access token Subject logout_token invalidate End-User OpenID Provider Relying party or resource server Discovery document permdock/jwt createPermDock decide permdock/ssf snapshot

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 claimPermDock fieldRule
subprincipal.idOpaque string; compared byte for byte; never a display name or email
issprincipal.issuerAlways set by permdock/jwt and the provider adapters; audit events carry issuer next to id
aud, azpVerified, not storedaud must contain audience; with accept: 'id-token' and several audiences, azp must equal the client id
exp, session_expirysubject.expiresAtmin(exp, session_expiry); copied into the snapshot
iat, nbfVerified, not storedWithin clockTolerance
acrprincipal.assurance.acrThe authentication context class reference, compared as an opaque string against the policy's step-up conditions
amrprincipal.assurance.amrArray of RFC 8176 method names (pwd, otp, hwk, mfa); conditions may require a member
auth_timeprincipal.assurance.authTimeSeconds since the epoch; the input to max_age style conditions
sidsubject.sessionSession identifier, used to match a later logout_token or CAEP session-revoked event to the snapshot
nonceRejected on access tokensIts presence is one of the signals for wrong-token-type
roles, groups, entitlementsprincipal.roles, principal.membershipsRFC 9068 claim names and SCIM encoding (JWT authorization claims)
org_id, tid, hd, org_codeprincipal.tenantNo standard claim exists; the provider page names the claim; a tenant with no matching membership is no tenant
verified_claimsprincipal.assurance.verifiedOpenID 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, pictureNowhereThe 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 conceptPermDock concept
OpenID ProviderThe issuer configured through discovery; principal.issuer
Relying Party / resource serverThe application running createPermDock; permdock/jwt is its token-to-subject step
End-Userprincipal with kind: 'user'
Client (azp, client_id)actor when it differs from the subject and acts under a delegation; otherwise verified and dropped
ID tokenAccepted only with accept: 'id-token'; never as authorization for an API
Access token (RFC 9068 at+jwt)The default input to subjectFromJwt
Discovery documentSource of jwks_uri and issuer; cached with the JWKS
acr, amr, auth_timeprincipal.assurance.{acr, amr, authTime}
verified_claimsprincipal.assurance.verified
RFC 9470 step-up challengedenied with reason insufficient-user-authentication, rendered as WWW-Authenticate
sidsubject.session; the join key for logout and CAEP events
Back-Channel Logout logout_tokenRevocation input to permdock/ssf
Public vs pairwise subDocumented on the provider page; grants and audit key on issuer + id either way, with no sector marker on the principal
Federation entity statementsTracking: candidate trust mechanism for permdock/pdp

Sources

Last updated on

On this page