# `Attesto.CIBA`
[🔗](https://github.com/XukuLLC/attesto/blob/v1.3.0/lib/attesto/ciba.ex#L1)

OpenID Connect Client-Initiated Backchannel Authentication (CIBA Core 1.0) -
the conn-free core.

CIBA is the device grant's sibling: an asynchronous grant where the client
never redirects the user's browser. The client POSTs a backchannel
authentication request naming the end-user through a hint, the OP
authenticates the user out-of-band on their *authentication device*, and
the client obtains tokens from the token endpoint with the
`"urn:openid:params:grant-type:ciba"` grant - by polling (poll
mode) or after an OP → client notification (ping mode).

Like `Attesto.DeviceCode`, every decision here is data-only: this module
reads no `Plug.Conn`, no clock except the passed `:now`, and drives all
state through an `Attesto.CIBAStore`. The endpoint flow decomposes as:

1. **Validate** - `Attesto.CIBA.Request.validate/3` checks the wire
   parameters (or the signed authentication request JWT) against the
   client's registered CIBA metadata.
2. **Resolve** - the *host* resolves the request's hint to an end-user
   (`unknown_user_id` / `expired_login_hint_token` on failure) and verifies
   any `user_code` (`missing_user_code` / `invalid_user_code`).
3. **Issue** - `issue/4` mints the `auth_req_id` (256-bit CSPRNG, above the
   §7.3 recommended 160 bits; only its hash is stored) and creates the
   `pending` record. The host then starts the out-of-band authentication.
4. **Decide** - `approve/4` / `deny/3` record the user's decision, returning
   the data a ping-mode host needs to deliver the §10.2 notification (which
   fires on approval AND denial).
5. **Redeem** - `redeem/4` runs the token-endpoint state machine with the
   exact §11 error vocabulary (`authorization_pending` / `slow_down` /
   `expired_token` / `access_denied` / `invalid_grant`), consuming the
   request single-use and yielding an `Attesto.CIBA.Grant` for the host's
   usual token-minting path.

## Delivery modes

Poll and ping records both enforce the §7.3 minimum token-request interval
frozen into the record at issue time (§10.2: a ping client that polls "must
be treated as if it had been registered to use the Poll mode"). Push mode is
accepted at validation for completeness - the token response would be
delivered by the host over its own transport - but **FAPI-CIBA §5.2.1
forbids push mode**; a FAPI deployment simply never registers a push client.

# `decision`

```elixir
@type decision() :: %{
  client_id: String.t(),
  delivery_mode: Attesto.CIBA.Request.delivery_mode(),
  client_notification_token: String.t() | nil
}
```

What `approve/4` / `deny/3` return: the data the host needs to deliver the
ping-mode notification (CIBA Core §10.2) - an HTTP POST to the client's
registered `backchannel_client_notification_endpoint` carrying
`Authorization: Bearer <client_notification_token>` and the JSON body
`{"auth_req_id": ...}`. `client_notification_token` is nil for poll mode
(no notification is sent).

# `error`

```elixir
@type error() ::
  :invalid_request
  | :invalid_scope
  | :expired_login_hint_token
  | :unknown_user_id
  | :unauthorized_client
  | :missing_user_code
  | :invalid_user_code
  | :invalid_binding_message
  | :invalid_client
  | :access_denied
  | :expired_token
  | :authorization_pending
  | :slow_down
  | :invalid_grant
```

The full CIBA error taxonomy. Backchannel authentication endpoint errors
(§13) come from `Attesto.CIBA.Request.validate/3`, from the host's hint /
user-code resolution, or from client authentication; token endpoint errors
(§11) come from `redeem/4`.

# `issue_attrs`

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

# `issued`

```elixir
@type issued() :: %{
  auth_req_id: String.t(),
  expires_in: pos_integer(),
  interval: pos_integer() | nil
}
```

What `issue/4` hands back: the §7.3 authentication request acknowledgement.

# `redeem_error`

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

# `approve`

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

Record a successful end-user authentication + consent for a pending
authentication request (CIBA Core §10: the "authentication result is
ready" transition).

`approval` carries `:subject` (required - and it MUST be the same end-user
the request was issued for; approving as anyone else fails with
`:subject_mismatch`, fail-closed), and optionally `:acr` (the satisfied
Authentication Context Class Reference; FAPI-CIBA §5.2.2 requires returning
`acr` when the client requested one), `:scope` (what the user actually
granted), `:claims`, and `:auth_time` (unix seconds, default `:now`).

Atomic `pending` → `approved`. Returns `{:ok, decision}` with the ping-mode
notification data (§10.2 - the notification fires on approval and denial
alike), or `{:error, reason}` (`:not_found` / `:already_decided` /
`:expired` / `:invalid_auth_req_id` / `:invalid_subject` /
`:subject_mismatch`).

# `deny`

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

Record a denial (the user refused, failed authentication, or the OP denied)
for a pending authentication request: atomic `pending` → `denied`, so the
client's next token request receives `access_denied` (§11).

Returns `{:ok, decision}` with the ping-mode notification data - the §10.2
notification is sent for denials too - or `{:error, reason}`.

# `error_status`

```elixir
@spec error_status(error()) :: 400 | 401 | 403
```

The HTTP status a backchannel authentication endpoint error is rendered
with (CIBA Core §13): 401 for `invalid_client`, 403 for `access_denied`,
400 for everything else. Token-endpoint errors (§11) are all 400 per
RFC 6749 §5.2 and do not use this mapping (`access_denied` is 400 there).

# `grant_type`

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

The CIBA grant type URN (CIBA Core §4 / §10.1), for token-endpoint dispatch
and `grant_types_supported` metadata.

# `issue`

```elixir
@spec issue(module(), Attesto.CIBA.Request.t(), issue_attrs(), keyword()) ::
  {:ok, issued()} | {:error, :invalid_subject}
```

Issue an `auth_req_id` for a validated authentication request whose hint the
host has resolved to an end-user (CIBA Core §7.3).

`attrs` carries what only the host knows: `:subject` (required - §7.1
requires the user to be identified BEFORE the acknowledgement is returned;
a hint that resolves to no user is the host's `unknown_user_id`), and the
optional `:resource` (RFC 8707) / `:dpop_jkt` (RFC 9449 §10 pre-binding).

Options: `:expires_in` (seconds, default 120 per
FAPI-CIBA's short-lifetime guidance), `:max_expires_in` (cap applied to the
client's `requested_expiry`, default 600), `:interval` (the
§7.3 minimum token-request interval, default 5), and
`:now`.

Returns `{:ok, %{auth_req_id: ..., expires_in: ..., interval: ...}}` - the
§7.3 acknowledgement fields. `interval` is nil for a push-mode request
(there is no token-endpoint polling to pace); the caller omits it from the
JSON response.

# `lookup`

```elixir
@spec lookup(module(), String.t()) ::
  {:ok, map()} | :error | {:error, :invalid_auth_req_id}
```

Non-consuming lookup of an authentication request, for the
authentication-device UI to show the user what they are approving (client,
scope, `binding_message`) and for the host to route the flow. Returns
`{:error, :invalid_auth_req_id}` for malformed input (before any store
call) and `:error` for an unknown id.

# `redeem`

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

Redeem an `auth_req_id` at the token endpoint
(`grant_type=urn:openid:params:grant-type:ciba`), running the CIBA Core §10.1/§11 state
machine.

`params` carries the requesting 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). Option: `:now`.

Returns `{:ok, %Attesto.CIBA.Grant{}}` once the user has authenticated and
the request is single-use consumed, or `{:error, reason}` where `reason` is
the exact §11 code the token endpoint renders verbatim:

  * `:authorization_pending` - the user has not yet been authenticated.
  * `:slow_down` - the client polled faster than the interval it was told
    at issuance (the client must add >=5s to its interval, §11).
  * `:expired_token` - the `auth_req_id`'s lifetime elapsed (this wins over
    a stale approval).
  * `:access_denied` - the user denied the request (or the OP did).
  * `:unauthorized_client` - the request was registered for push-mode
    delivery, which (CIBA Core §11) MUST NOT be redeemed at the token
    endpoint; the token comes over the host's push transport instead.
  * `:invalid_grant` - unknown/garbage `auth_req_id`, a client mismatch
    ("issued to another Client", §11), DPoP mismatch, or an
    already-consumed request.

All record validation - expiry, client binding, DPoP binding, push-mode
rejection, and terminal status - runs on a NON-mutating read BEFORE any
poll-interval throttling. `slow_down` is a variant of `authorization_pending`
(§11): it is returned only when the request is genuinely still pending, so it
can never mask an expired, denied, approved, consumed, wrong-client,
wrong-DPoP, or push-mode outcome, and an unauthorized caller can never mutate
another client's throttle state.

# `validate_request`

```elixir
@spec validate_request(Attesto.CIBA.Request.client(), map(), keyword()) ::
  {:ok, Attesto.CIBA.Request.t()} | {:error, Attesto.CIBA.Request.error()}
```

Validate a backchannel authentication request's wire parameters against the
authenticated client's registered CIBA metadata. See
`Attesto.CIBA.Request.validate/3` for the parameter/option contract.

---

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