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

RFC 8628 Device Authorization Grant — the conn-free core.

This is the storage-backed primitive behind the device flow, the analogue of
`Attesto.AuthorizationCode` for a browserless, redirect-less, user-present
grant. A headless/CLI client requests a `device_code` + a human-readable
`user_code` (`issue/3`), shows the user the `user_code` and a verification
URL, and polls the token endpoint (`redeem/4`) while the user approves on a
second device (`approve/3` / `deny/2` from the verification page).

Like `Attesto.AuthorizationCode`, every decision is data-only: this module
reads no `Plug.Conn`, no clock except the passed `:now`, and drives all state
through an `Attesto.DeviceCodeStore`. The polling state machine and its exact
RFC 8628 §3.5 error vocabulary (`authorization_pending` / `slow_down` /
`expired_token` / `access_denied`) live here.

## `user_code`

The `user_code` is generated from an ambiguity-free base-20 alphabet
(RFC 8628 §6.1: no vowels — so no accidental words — and no visually
confusable `0/O/1/I`), 8 characters by default (~34.6 bits), displayed
hyphenated (`BCDF-GHJK`). It is **normalized and charset-validated here before
any store lookup** (`normalize_user_code/2`): upper-cased, separators
stripped, and rejected outright if it falls outside the alphabet or length —
so attacker-shaped input never reaches the store's unique-index query, the
same fail-closed discipline `Attesto.ResourceIndicator.validate/1` applies.

# `issue_attrs`

```elixir
@type issue_attrs() :: %{
  :client_id =&gt; String.t(),
  optional(:scope) =&gt; [String.t()],
  optional(:resource) =&gt; [String.t()],
  optional(:dpop_jkt) =&gt; String.t() | nil
}
```

# `issued`

```elixir
@type issued() :: %{device_code: String.t(), user_code: String.t()}
```

What `issue/3` hands back: the secret device code and the display user code.

# `redeem_error`

```elixir
@type redeem_error() ::
  :authorization_pending
  | :slow_down
  | :expired_token
  | :access_denied
  | :invalid_grant
```

# `approve`

```elixir
@spec approve(module(), String.t(), map(), keyword()) ::
  :ok
  | {:error,
     :not_found
     | :already_decided
     | :expired
     | :invalid_user_code
     | :invalid_subject}
```

Approve a pending device code from the verification page (RFC 8628 §3.3),
binding the resolved resource owner.

`approval` carries `:subject` (required), and the granted `:scope` /
`:claims`. Atomic `pending` → `approved`. Returns `:ok`, or `{:error, reason}`
(`:not_found` / `:already_decided` / `:expired` / `:invalid_user_code` /
`:invalid_subject`).

# `deny`

```elixir
@spec deny(module(), String.t(), keyword()) ::
  :ok | {:error, :not_found | :already_decided | :expired | :invalid_user_code}
```

Deny a pending device code from the verification page (RFC 8628 §3.3): atomic
`pending` → `denied`, so the device's next poll receives `access_denied`.

# `generate_user_code`

```elixir
@spec generate_user_code(pos_integer()) :: String.t()
```

Generate a fresh display-formatted user code (`BCDF-GHJK`).

Each character is drawn from the base-20 alphabet using
`:crypto.strong_rand_bytes/1` (a CSPRNG) with rejection sampling to keep the
draw uniform — the `user_code` is an online authorization handle, so it must
not come from the VM's non-cryptographic PRNG.

# `issue`

```elixir
@spec issue(module(), issue_attrs(), keyword()) ::
  {:ok, issued()} | {:error, :invalid_client_id | :user_code_unavailable}
```

Issue a device code + user code for an authenticated device-authorization
request (RFC 8628 §3.1 / §3.2).

`attrs` carries the issue-time binding: `:client_id` (required), and the
optional `:scope` / `:resource` (RFC 8707) / `:dpop_jkt` (RFC 9449 §10
pre-binding). Options: `:ttl` (seconds, default 600),
`:user_code_length` (default 8), and `:now`.

Returns `{:ok, %{device_code: ..., user_code: ...}}` with the plaintext device
code (only its hash is stored) and the display-formatted user code.

# `lookup`

```elixir
@spec lookup(module(), String.t()) ::
  {:ok, Attesto.DeviceCodeStore.pending_view()}
  | :error
  | {:error, :invalid_user_code}
```

Non-consuming lookup of a pending device code by `user_code`, for the
verification page to show the user what they are approving. Returns
`{:error, :invalid_user_code}` for malformed input (before any store call) and
`:error` for an unknown code.

# `normalize_user_code`

```elixir
@spec normalize_user_code(
  String.t(),
  keyword()
) :: {:ok, String.t()} | {:error, :invalid_user_code}
```

Normalize and charset-validate a user-entered `user_code`: upper-case, strip
separators (hyphens/whitespace), and reject anything outside the base-20
alphabet or the expected length. Fail-closed — returns
`{:error, :invalid_user_code}` rather than letting attacker-shaped input reach
a store lookup.

# `redeem`

```elixir
@spec redeem(module(), String.t(), map(), keyword()) ::
  {:ok, Attesto.DeviceCode.Grant.t()} | {:error, redeem_error()}
```

Redeem a device code at the token endpoint, running the RFC 8628 §3.5 polling
state machine.

`params` carries the polling client's `:client_id` (matched against the
issue-time binding) and any `:dpop_jkt` (RFC 9449 holder-of-key, matched
against a pre-bound key). Options: `:now` and `:interval` (the minimum poll
interval in seconds, default 5).

Returns `{:ok, %Attesto.DeviceCode.Grant{}}` once the user has approved and the
code is single-use consumed, or `{:error, reason}` where `reason` is the exact
RFC 8628 §3.5 code the token endpoint renders verbatim:

  * `:authorization_pending` - the user has not yet approved.
  * `:slow_down` - the device polled faster than `:interval`.
  * `:expired_token` - the code's TTL elapsed (this wins over a stale approval).
  * `:access_denied` - the user denied the request.
  * `:invalid_grant` - unknown/garbage device code, client mismatch, DPoP
    mismatch, or an already-consumed code.

---

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