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

Refresh-token issuance and rotation with reuse detection
(RFC 6749 §6 / §10.4, OAuth 2.0 Security BCP).

Each refresh token is single-use: presenting it (`rotate/3`) consumes it
and mints a successor in the same *family*. A short idempotency window
(10 seconds by default) lets the same client retry the just-consumed
parent after a lost response and receive the same successor. Outside
that window, or when the retry does not match the original client,
binding, and scope, a rotated token is a captured-token signal and the
entire family is revoked so neither the attacker nor the victim can
continue, forcing a fresh authorization.

This module is pure logic over a `Attesto.RefreshStore`; the store
provides the atomic `consume/1` on which reuse detection depends (see
that behaviour's moduledoc). Only the hash of each token is stored.

## DPoP binding

A refresh token can be bound to a DPoP key (its issuing context carries
a `:dpop_jkt`). Rotation then requires the caller to present the
matching `:dpop_jkt` (the thumbprint of the key in the token-request's
DPoP proof); an unbound token must be rotated without one. The binding
matrix mirrors `Attesto.Token` and `Attesto.AuthorizationCode`.

# `context`

```elixir
@type context() :: %{
  :subject =&gt; String.t(),
  optional(:scope) =&gt; [String.t()],
  optional(:resource) =&gt; [String.t()],
  optional(:acr) =&gt; String.t() | nil,
  optional(:auth_time) =&gt; non_neg_integer() | nil,
  optional(:client_id) =&gt; String.t(),
  optional(:dpop_jkt) =&gt; String.t() | nil,
  optional(:claims) =&gt; map()
}
```

# `issue_error`

```elixir
@type issue_error() ::
  :invalid_subject
  | :invalid_scope
  | :invalid_resource
  | :invalid_dpop_jkt
  | :invalid_claims
  | :family_revoked
```

# `issued`

```elixir
@type issued() :: %{
  token: String.t(),
  family_id: String.t(),
  generation: non_neg_integer()
}
```

# `rotate_error`

```elixir
@type rotate_error() ::
  :invalid_grant
  | :reuse_detected
  | :expired
  | :client_required
  | :client_mismatch
  | :invalid_scope
  | :invalid_target
  | :dpop_proof_required
  | :dpop_proof_unexpected
  | :dpop_binding_mismatch
```

# `rotated`

```elixir
@type rotated() :: %{
  token: String.t(),
  family_id: String.t(),
  generation: non_neg_integer(),
  context: map()
}
```

# `issue`

```elixir
@spec issue(module(), context(), keyword()) ::
  {:ok, issued()} | {:error, issue_error()}
```

Issue a refresh token for `context` and persist it via `store`.

`context` MUST carry `:subject`; optional `:scope` (list, default
`[]`), `:client_id`, `:dpop_jkt` (binds the token to a DPoP key), and
`:claims` (opaque host context).

Options: `:ttl` (seconds, default 14 days), `:now`, and - when
continuing a family during rotation - `:family_id` and `:generation`
(callers issuing a first token omit both: a fresh family is started at
generation 0).

Returns `{:ok, %{token, family_id, generation}}` with the plaintext
token to hand the client (only its hash is stored), or
`{:error, reason}` on malformed `context`. Returns
`{:error, :family_revoked}` only when continuing an explicit
`:family_id` that has been revoked (a fresh first issue starts a new
family and never hits this).

# `rotate`

```elixir
@spec rotate(module(), String.t(), keyword()) ::
  {:ok, rotated()} | {:error, rotate_error()}
```

Rotate a presented refresh token: consume it and mint its successor.

On success returns `{:ok, %{token, family_id, generation, context}}`
where `token` is the new refresh token, `generation` is the successor's
generation, and `context` is the grant context to mint the next access
token from.

If the presented token was already rotated, an immediate matching retry
returns the original successor within `:rotation_grace_seconds`;
otherwise the whole family is revoked and `{:error, :reuse_detected}` is
returned. Other failures: `:invalid_grant` (unknown token), `:expired`,
`:client_mismatch`, `:invalid_scope`, and the DPoP binding errors.

Options:

  * `:now` - clock override.
  * `:dpop_jkt` - the presented proof's thumbprint (for DPoP-bound
    tokens).
  * `:client_id` - the authenticated presenting client. When the token
    was issued with a `client_id`, rotation is fail-closed: it MUST
    present a matching one (`:client_required` if absent,
    `:client_mismatch` if wrong), closing token substitution across
    clients (RFC 6749 §6 / §10.4). Pass `allow_missing_client_id?: true`
    to opt out. A token issued without a client binding skips the check.
  * `:scope` - a requested scope list. MUST be a subset of the token's
    granted scope; the successor then carries the narrowed scope. A
    request for any scope not granted is `:invalid_scope` (no
    escalation). Omitted, the successor carries the full granted scope.
  * `:ttl` - lifetime for the successor.
  * `:rotation_grace_seconds` - idempotency window for an immediate retry of
    the just-rotated token. Defaults to `10`; set `0` for strict reuse
    revocation.

Recoverable failures (`:client_mismatch`, `:invalid_scope`, `:expired`,
the DPoP binding errors) are checked on a non-consuming read *before*
the token is claimed, so they do NOT burn the token: a client that, say,
retries with a corrected DPoP proof succeeds rather than tripping reuse
detection. Only a genuine replay of an already-consumed token (or a
concurrent double-claim) revokes the family.

---

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