Attesto.IDToken (Attesto v0.6.0)

Copy Markdown View Source

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 OAuth client_id of the Relying Party. This is the client, NOT the Attesto.Config audience an 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 when aud contains a value other than the client_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).

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.

Types

claims()

@type claims() :: %{optional(String.t()) => term()}

mint_error()

@type mint_error() ::
  :invalid_subject
  | :invalid_client_id
  | :invalid_extra_claims
  | :reserved_claim_conflict

mint_opts()

@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(),
  extra_claims: %{optional(String.t()) => term()}
]

verify_error()

@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

verify_opts()

@type verify_opts() :: [
  now: DateTime.t() | non_neg_integer(),
  client_id: String.t(),
  nonce: String.t()
]

Functions

header_typ()

@spec header_typ() :: String.t()

The JOSE header typ ID Tokens carry: "JWT" (never "at+jwt").

mint(config, subject, client_id, opts \\ [])

@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 the nonce claim, and verify/3 then 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 when aud has 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, the at_hash claim is computed from it (OIDC Core §3.1.3.6).
  • :code - when given, the c_hash claim is computed from it (OIDC Core §3.3.2.11).
  • :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 - DateTime or 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}.

signing_alg()

@spec signing_alg() :: String.t()

The default JWS algorithm for RSA keys. Keystores may label individual keys with another supported alg.

verify(config, id_token, opts \\ [])

@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:

  1. 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. A kid naming a key we do not hold, or an alg other than RS256, fails as :invalid_signature (alg-confusion is impossible). A protected header carrying a crit parameter (RFC 7515 §4.1.11) is rejected with :unsupported_critical_header. The JOSE header typ, when present, MUST be "JWT"; an access-token header such as "at+jwt" is :unexpected_typ.
  2. iss equals the configured issuer (OIDC Core §3.1.3.7 item 1).
  3. aud contains the expected client_id (OIDC Core §3.1.3.7 item 3).
  4. azp - when present, equals the client_id (OIDC Core §3.1.3.7 item 4/5).
  5. Required claims are present and well-typed: sub a non-empty string, iat a non-negative integer.
  6. Temporal. exp is strictly greater than now (no skew leeway); an iat meaningfully in the future is :not_yet_valid.
  7. nonce - when a :nonce is 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 in aud (REQUIRED; OIDC Core §3.1.3.7 item 3).
  • :nonce - the nonce sent in the Authentication Request. When supplied, the nonce claim MUST be present and equal.
  • :now - clock override.

Returns {:ok, claims} (string-keyed payload) or {:error, reason}.