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

Mint and verify RS256 JWT access tokens.

This is the heart of the engine: a single mint point and a single
verifier that one issuer uses for every kind of principal. The two
operations are pure - they read no database, no process state, and no
application config beyond the `Attesto.Config` you pass in. Effects that
surround issuance (auditing, persisting refresh state, looking up
revocation) belong to the host application, which wraps these functions.

## Claims

Every minted token carries:

  * `iss` - the configured issuer.
  * `aud` - the configured audience, or the per-call `:audience` mint
    option when the host derived a resource-specific audience (RFC 8707
    §2 resource indicator → access-token `aud`).
  * `sub` - the subject's public identifier, which MUST begin with the
    `sub_prefix` of its principal kind.
  * `exp` / `iat` - expiry and issued-at, unix seconds.
  * `jti` - a 128-bit crypto-random identifier, base64url-no-pad
    (RFC 7519 §4.1.7), so a resource server can reject replay.
  * `scope` - the space-separated granted scope list (resolved by the
    host's policy and passed in; Attesto does not decide who gets what).
  * `typ` - the token purpose, `"access"` or `"refresh"`.
  * the configured principal-kind claim - the kind's `claim_value`,
    cross-checked against `sub` on verify.
  * any per-kind required claims (e.g. `client_id`).
  * `cnf` - present iff the token is sender-constrained (DPoP or mTLS).

Tokens are signed RS256 with the key the configured `Attesto.Keystore`
provides; the JWS header carries the key's `kid` (its RFC 7638
thumbprint). The algorithm is pinned: `verify/3` rejects anything but
RS256, so `none`/`HS256` alg-confusion is impossible by construction.

## Sender constraints

`mint/3` accepts at most one of `:dpop_jkt` (RFC 9449) or
`:mtls_cert_thumbprint` (RFC 8705); supplying both is
`:conflicting_confirmation`. The chosen binding becomes a `cnf` claim
(RFC 7800), and `verify/3` enforces it: a DPoP- or mTLS-bound token
presented without (or with a mismatched) proof is rejected, and a proof
presented against a token that is not bound that way is rejected too.
See `verify/3` for the full binding matrix.

## What this module does NOT do

Scope *policy* (which scopes a principal may hold, downscoping rules) is
the host's; pass the already-resolved scope list to `mint/3`. Revocation
lookup, `jti` replay rejection of the access token, and audit are the
resource server's. Keeping them out is what lets the verifier stay pure
and reusable (token introspection, multiple surfaces).

# `claims`

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

# `mint_error`

```elixir
@type mint_error() ::
  :unknown_principal_kind
  | :invalid_sub
  | :invalid_claims
  | :reserved_claim_conflict
  | :invalid_scopes
  | :invalid_typ
  | :invalid_audience
  | :invalid_dpop_jkt
  | :invalid_mtls_thumbprint
  | :conflicting_confirmation
```

# `mint_opts`

```elixir
@type mint_opts() :: [
  now: DateTime.t() | non_neg_integer(),
  lifetime: pos_integer(),
  typ: String.t(),
  audience: String.t() | [String.t()],
  acr: String.t(),
  auth_time: non_neg_integer(),
  dpop_jkt: String.t() | nil,
  mtls_cert_thumbprint: String.t() | nil
]
```

# `principal`

```elixir
@type principal() :: %{
  :kind =&gt; String.t(),
  :sub =&gt; String.t(),
  :scopes =&gt; [String.t()],
  optional(:claims) =&gt; %{optional(String.t()) =&gt; term()}
}
```

# `token_response`

```elixir
@type token_response() :: %{
  access_token: String.t(),
  expires_in: pos_integer(),
  scope: String.t(),
  token_type: String.t()
}
```

# `verify_error`

```elixir
@type verify_error() ::
  :invalid_token
  | :invalid_signature
  | :invalid_issuer
  | :invalid_audience
  | :expired
  | :not_yet_valid
  | :invalid_claims
  | :invalid_principal
  | :invalid_typ
  | :unexpected_typ
  | :unsupported_critical_header
  | :unsupported_confirmation
  | :dpop_proof_required
  | :dpop_binding_mismatch
  | :dpop_proof_unexpected
  | :mtls_cert_required
  | :mtls_binding_mismatch
  | :mtls_cert_unexpected
```

# `verify_opts`

```elixir
@type verify_opts() :: [
  now: DateTime.t() | non_neg_integer(),
  expected_typ: String.t(),
  dpop_jkt: String.t() | nil,
  mtls_cert_thumbprint: String.t() | nil,
  require_confirmation_binding: boolean()
]
```

# `default_lifetime_seconds`

```elixir
@spec default_lifetime_seconds(Attesto.Config.t()) :: pos_integer()
```

The default token lifetime for `config`, in seconds.

# `mint`

```elixir
@spec mint(Attesto.Config.t(), principal(), mint_opts()) ::
  {:ok, token_response()} | {:error, mint_error()}
```

Mint a token for `principal` under `config`.

`principal` is a map with:

  * `:kind` - the `claim_value` of one of the configured principal
    kinds.
  * `:sub` - the subject's public identifier; MUST begin with the
    kind's `sub_prefix`.
  * `:scopes` - the final, policy-resolved list of scope strings. Joined
    verbatim into the `scope` claim; Attesto applies no scope policy.
  * `:claims` (optional) - extra principal claims (e.g.
    `%{"client_id" => ...}`). MUST satisfy the kind's `required_claims`
    and MUST NOT collide with a reserved protocol claim.

Options:

  * `:typ` - `"access"` (default) or `"refresh"`.
  * `:audience` - the `aud` claim for this token, overriding
    `config.audience`. RFC 8707 §2: when a token request carries a
    `resource` indicator the access token's `aud` MUST identify that
    resource, so the host passes the validated resource identifier here;
    absent, `config.audience` is used (the default surface). The override
    is per-call and conn-free - it does not mutate `config` - so a single
    issuer can mint resource-audienced tokens for one grant without
    changing `aud` for any other.
  * `:now` - `DateTime` or unix-seconds clock override. Defaults to now.
  * `:lifetime` - positive seconds; may only *shorten* the configured
    default (a larger value is capped to the default, so a miswired
    caller cannot mint a long-lived token).
  * `:dpop_jkt` - RFC 7638 JWK thumbprint to bind the token to a DPoP
    key (`cnf.jkt`). Must be a canonical 43-char base64url thumbprint or
    `:invalid_dpop_jkt`.
  * `:mtls_cert_thumbprint` - RFC 8705 certificate thumbprint to bind
    the token to a client certificate (`cnf.x5t#S256`). Same shape rule
    or `:invalid_mtls_thumbprint`.

`:dpop_jkt` and `:mtls_cert_thumbprint` are mutually exclusive
(`:conflicting_confirmation`).

Returns `{:ok, %{access_token, token_type, expires_in, scope}}`.
`token_type` is `"DPoP"` for a DPoP-bound token (RFC 9449 §5) and
`"Bearer"` otherwise (mTLS binding does not change the type per
RFC 8705 §3).

# `peek_signed_claims`

```elixir
@spec peek_signed_claims(Attesto.Config.t(), String.t()) ::
  {:ok, claims()} | {:error, :invalid_signature | :invalid_token}
```

Return a token's claims iff its RS256 signature verifies against a
keystore key. Skips every other check (`iss`, `aud`, `exp`, claim
shape, binding).

This is NOT an authentication primitive - the token may be expired,
replayed, wrongly scoped, or bound to a key the request did not present.
Its sole legitimate use is denial-audit attribution: after `verify/3`
fails, a caller may read the claims to identify the credential being
abused so the audit row names a real actor rather than `:unknown`. A
forged-signature token still surfaces as an error.

# `signing_alg`

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

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

# `typ_values`

```elixir
@spec typ_values() :: [String.t()]
```

The known `typ` values: `"access"` and `"refresh"`.

# `verify`

```elixir
@spec verify(Attesto.Config.t(), String.t(), verify_opts()) ::
  {:ok, claims()} | {:error, verify_error()}
```

Verify and decode a token previously minted by `mint/3` under the same
`config`.

Runs, in order:

  1. **Signature.** The compact JWS is canonical - three base64url-no-pad
     segments (Attesto rejects `=` padding or any non-base64url byte at
     its boundary, refusing to verify a serialization the issuer never
     emitted) - and its RS256 signature
     verifies against a key the keystore trusts, selected by the JWS
     header `kid`. A token whose `kid` names a key we do not hold, or
     whose header `alg` is anything but RS256, fails as
     `:invalid_signature` (alg-confusion is impossible). A token whose
     protected header carries a `crit` parameter (RFC 7515 §4.1.11) is
     rejected with `:unsupported_critical_header` - Attesto implements no
     JWS extensions, so it must not honour a token that demands one.
  2. **Confirmation shape.** If a `cnf` is present it MUST be exactly
     `%{"jkt" => <thumbprint>}` (DPoP) or `%{"x5t#S256" => <thumbprint>}`
     (mTLS), with a canonical thumbprint and no other members; anything
     else is `:unsupported_confirmation` (accepting it as bearer would
     silently strip the binding).
  3. **`iss`** equals the configured issuer.
  4. **`aud`** equals (or, in array form, contains) the configured
     audience.
  5. **Temporal.** `exp` is strictly greater than `now` (no skew
     leeway). If `nbf` is present it MUST be an integer no later than
     `now` (RFC 7519 §4.1.5; a small clock-skew tolerance applies), else
     `:not_yet_valid`. An `iat` meaningfully in the future is also
     `:not_yet_valid`.
  6. **Required claims** are present and well-typed: `sub`/`jti`
     non-empty strings, `scope` a string, `iat` a non-negative integer,
     and both the principal-kind claim and `typ` present.
  7. **Principal.** The principal-kind claim names a configured kind AND
     `sub` begins with that kind's `sub_prefix`; otherwise
     `:invalid_principal`.
  8. **Per-kind claims.** The kind's `required_claims` are all present
     with the right shape; otherwise `:invalid_claims`.
  9. **`typ`** is a known value AND equals the expected purpose
     (`:expected_typ`, default `"access"`).
 10. **Binding.** A DPoP-bound token requires a matching `:dpop_jkt`; an
     mTLS-bound token a matching `:mtls_cert_thumbprint`; an unbound
     token requires neither. The cross-scheme option MUST be absent.
     See the error list for the precise outcomes.

## Options

  * `:now` - clock override.
  * `:expected_typ` - `"access"` (default) or `"refresh"`.
  * `:dpop_jkt` - the verified DPoP proof's `jkt` (from
    `Attesto.DPoP.verify_proof/2`). Required iff the token carries
    `cnf.jkt`.
  * `:mtls_cert_thumbprint` - the presented certificate's thumbprint
    (from `Attesto.MTLS.compute_thumbprint/1`). Required iff the token
    carries `cnf.x5t#S256`.

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

---

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