Mint and verify OpenID Connect ID Tokens (OpenID Connect Core 1.0 §2).
An ID Token is the JWT that asserts the authentication of an End-User to
a Relying Party. It is a different artifact from the RFC 9068 access
token Attesto.Token produces, with different semantics: its aud is
the OAuth client_id rather than the protected-resource audience, it
carries no scope claim, and its JOSE header typ is the generic
JWT (NOT at+jwt, which is reserved for access tokens). The two are
kept in separate modules rather than overloading one mint path.
Like Attesto.Token, the operations are pure: they read only the
Attesto.Config passed in. Signing uses the same keystore/kid path
and the same RS256 pinning, and every JOSE call funnels through
JOSE.JWS / JOSE.JWT.verify_strict so the alg whitelist (no none,
no HS256 confusion) lives in one place and fails closed.
Claims (OpenID Connect Core §2)
Every minted ID Token carries:
iss- the configured issuer.sub- the subject identifier for the End-User.aud- the OAuthclient_idof the Relying Party. This is the client, NOT theAttesto.Configaudiencean access token uses.exp/iat- expiry and issued-at, unix seconds.
Conditionally / optionally present:
nonce- the value from the Authentication Request. REQUIRED to be present and identical when the request carried one (OIDC Core §2, §3.1.3.7 item 11).azp- the authorized party. REQUIRED whenaudcontains a value other than theclient_id(OIDC Core §2); always safe to include.auth_time- time of End-User authentication (OIDC Core §2).acr- Authentication Context Class Reference (OIDC Core §2).amr- Authentication Methods References, a JSON array (OIDC Core §2).at_hash- Access Token hash (OIDC Core §3.1.3.6 / §3.3.2.11).c_hash- Authorization Code hash (OIDC Core §3.3.2.11).sid- Session ID (OIDC Back-Channel Logout 1.0 §2.1 / Front-Channel Logout 1.0 §3). Present when the host runs back-channel-logout-capable sessions, so a laterlogout_tokencan target this exact session.
There is deliberately no scope claim: scope is a property of the
authorization grant, not of the identity assertion.
Additional claims (claims parameter / userinfo mapping)
Claims an RP requests through the OIDC Core §5.5 claims request
parameter, or that a host maps from its userinfo source, are passed to
mint/3 as :extra_claims: a string-keyed map merged after the protocol
claims above. The merge is non-overriding by construction - a key that
collides with a reserved protocol claim (iss, sub, aud, exp,
iat, nonce, azp, auth_time, acr, amr, at_hash, c_hash) is
rejected with :reserved_claim_conflict rather than silently shadowing
the value this module computes; a non-map or non-string-keyed value is
:invalid_extra_claims. This keeps claim provenance in the caller (the
host/RP decides which profile claims to assert) while the protocol claims
stay authoritative.
Hash claims
at_hash and c_hash use the same construction (OIDC Core §3.1.3.6,
§3.3.2.11): hash the ASCII octets of the access_token / code with
the hash of the ID Token's signature algorithm (SHA-256 for RS256),
take the left-most half of the digest, and base64url-encode it without
padding.
Summary
Functions
The JOSE header typ ID Tokens carry: "JWT" (never "at+jwt").
Mint a signed OpenID Connect ID Token for subject, addressed to the
Relying Party identified by client_id.
The default JWS algorithm for RSA keys. Keystores may label individual keys with another supported alg.
Verify and decode an ID Token previously minted under the same config.
Verify an ID Token presented as an id_token_hint to the end-session
endpoint (OpenID Connect RP-Initiated Logout 1.0 §2), returning its claims.
Types
@type mint_error() ::
:invalid_subject
| :invalid_client_id
| :invalid_extra_claims
| :reserved_claim_conflict
@type mint_opts() :: [ now: DateTime.t() | non_neg_integer(), lifetime: pos_integer(), nonce: String.t(), azp: String.t(), auth_time: non_neg_integer(), acr: String.t(), amr: [String.t()], access_token: String.t(), code: String.t(), sid: String.t(), extra_claims: %{optional(String.t()) => term()} ]
@type verify_error() ::
:invalid_token
| :invalid_signature
| :unsupported_critical_header
| :unexpected_typ
| :invalid_issuer
| :invalid_audience
| :invalid_azp
| :expired
| :not_yet_valid
| :invalid_claims
| :missing_client_id
| :nonce_required
| :nonce_mismatch
@type verify_opts() :: [ now: DateTime.t() | non_neg_integer(), client_id: String.t(), nonce: String.t() ]
Functions
@spec header_typ() :: String.t()
The JOSE header typ ID Tokens carry: "JWT" (never "at+jwt").
@spec mint(Attesto.Config.t(), String.t(), String.t(), mint_opts()) :: {:ok, String.t()} | {:error, mint_error()}
Mint a signed OpenID Connect ID Token for subject, addressed to the
Relying Party identified by client_id.
client_id becomes the aud claim (OIDC Core §2), distinguishing the
ID Token from a resource-addressed access token; config.audience is
not used here.
Options:
:nonce- the Authentication Request nonce. When supplied it is placed in thenonceclaim, andverify/3then requires a match (OIDC Core §2). Omit only when the request carried no nonce.:azp- the authorized party (OIDC Core §2). REQUIRED by the spec whenaudhas more than one audience.:auth_time- unix time of End-User authentication (OIDC Core §2).:acr- Authentication Context Class Reference (OIDC Core §2).:amr- Authentication Methods References, a list (OIDC Core §2).:access_token- when given, theat_hashclaim is computed from it (OIDC Core §3.1.3.6).:code- when given, thec_hashclaim is computed from it (OIDC Core §3.3.2.11).:sid- the session id to assert (OIDC Back-Channel Logout 1.0 §2.1). Supply it when the host issues logout-capable sessions so a futureAttesto.LogoutTokencan target this session.:extra_claims- a string-keyed map of additional claims (e.g. profile claims). MUST NOT collide with a reserved protocol claim (:reserved_claim_conflict) and MUST have string keys.:now-DateTimeor unix-seconds clock override. Defaults to now.:lifetime- positive seconds; may only shorten the default (a larger value is capped to the default), so a miswired caller cannot mint a long-lived identity assertion.
Returns {:ok, id_token} (compact JWS) or {:error, reason}.
@spec signing_alg() :: String.t()
The default JWS algorithm for RSA keys. Keystores may label individual keys with another supported alg.
@spec verify(Attesto.Config.t(), String.t(), verify_opts()) :: {:ok, claims()} | {:error, verify_error()}
Verify and decode an ID Token previously minted under the same config.
Mirrors Attesto.Token.verify/3 where the OIDC semantics line up. Runs,
in order:
- Signature. The compact JWS is canonical - three base64url-no-pad
segments - and its RS256 signature verifies against a keystore key
selected by the JWS header
kid. Akidnaming a key we do not hold, or analgother than RS256, fails as:invalid_signature(alg-confusion is impossible). A protected header carrying acritparameter (RFC 7515 §4.1.11) is rejected with:unsupported_critical_header. The JOSE headertyp, when present, MUST be"JWT"; an access-token header such as"at+jwt"is:unexpected_typ. Verification also rejects access-token-only payload claims such asscope,typ: "access", and the configured principal-kind claim, so token-type separation does not depend solely on the optional JOSEtypheader. issequals the configured issuer (OIDC Core §3.1.3.7 item 1).audcontains the expectedclient_id(OIDC Core §3.1.3.7 item 3).azp- when present, equals theclient_id(OIDC Core §3.1.3.7 item 4/5).- Required claims are present and well-typed:
suba non-empty string,iata non-negative integer. - Temporal.
expis strictly greater thannow(no skew leeway); aniatmeaningfully in the future is:not_yet_valid. nonce- when a:nonceis supplied, the claim is present and identical (OIDC Core §3.1.3.7 item 11).
Options:
:client_id- the Relying Party client id to require inaud(REQUIRED; OIDC Core §3.1.3.7 item 3).:nonce- the nonce sent in the Authentication Request. When supplied, thenonceclaim MUST be present and equal.:now- clock override.
Returns {:ok, claims} (string-keyed payload) or {:error, reason}.
@spec verify_logout_hint(Attesto.Config.t(), String.t()) :: {:ok, claims()} | {:error, verify_error()}
Verify an ID Token presented as an id_token_hint to the end-session
endpoint (OpenID Connect RP-Initiated Logout 1.0 §2), returning its claims.
This is a deliberately looser check than verify/3: the signature, token
purpose, issuer, required-claim shape, and a non-future iat are all
enforced, but
- the
aud(client) is not required up front — the caller reads the Relying Party from the returnedaudclaim, and - expiry is tolerated — RP-Initiated Logout §2 says the OP SHOULD accept an otherwise-valid hint even after it has expired, since the user is logging out precisely because their session is ending.
A logout token's authenticity still rests entirely on the signature, so a
forged or tampered hint is rejected as :invalid_signature / :invalid_token.
Returns {:ok, claims} (string-keyed payload) or {:error, reason}.