# `Attesto.IDToken`
[🔗](https://github.com/XukuLLC/attesto/blob/v0.13.0/lib/attesto/id_token.ex#L1)

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).
  * `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 later `logout_token` can 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.

# `claims`

```elixir
@type claims() :: %{optional(String.t()) =&gt; term()}
```

# `mint_error`

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

# `mint_opts`

```elixir
@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()) =&gt; term()}
]
```

# `verify_error`

```elixir
@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`

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

# `header_typ`

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

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

# `mint`

```elixir
@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).
  * `: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 future
    `Attesto.LogoutToken` can 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` - `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`

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

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

# `verify`

```elixir
@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`. Verification also rejects access-token-only
     payload claims such as `scope`, `typ: "access"`, and the configured
     principal-kind claim, so token-type separation does not depend solely
     on the optional JOSE `typ` header.
  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}`.

# `verify_logout_hint`

```elixir
@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 returned `aud` claim, 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}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
