# `AttestoPhoenix.OAuthError`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/oauth_error.ex#L2)

The error value type and the wire-rendering helpers for the
authorization-server controllers and the protected-resource plugs.

This module is both:

  * **a struct** - the controllers build `%AttestoPhoenix.OAuthError{}`
    with `new/2` / `new/3` and thread it through their `with` chains as
    the `{:error, error}` term, then render it once at the boundary with
    `render/2`; and

  * **a set of header helpers** - `unauthorized/4`, `use_dpop_nonce/3`,
    `insufficient_scope/3`, `no_store/2`, and `www_authenticate/3` -
    used by the protected-resource plugs to emit `WWW-Authenticate`
    challenges and cache-suppression headers directly.

It is the single place the library turns an internal error into the bytes
a client receives. It covers four surfaces, each governed by a different
RFC:

  * **Token / endpoint errors** (RFC 6749 §5.2) - the JSON body
    `{"error": <code>, "error_description": <text>}` returned by the
    token, revocation, and registration endpoints. When the request
    attempted HTTP `Authorization`-based client authentication and the
    status is 401, RFC 6749 §5.2 requires a matching `WWW-Authenticate`
    challenge, so `render/2` re-derives it from the request rather than
    trusting the caller to remember.

  * **Protected-resource challenges** (RFC 6750 §3 / RFC 9449 §7.1) - a
    `WWW-Authenticate` response header naming the `Bearer` or `DPoP`
    scheme and carrying the `error`, `error_description`, `scope`, and
    (for DPoP) `algs` auth-params.

  * **DPoP nonce challenges** (RFC 9449 §8 / §9) - the `use_dpop_nonce`
    error returned with a fresh `DPoP-Nonce` response header, telling
    the client to retry the request carrying that nonce.

  * **Cache suppression** (RFC 6749 §5.1) - `no_store/2` marks a
    response uncacheable with `Cache-Control: no-store` and
    `Pragma: no-cache`, mandatory on every response that carries a
    token, and applied to every error response here for defense in depth.

Every quoted auth-param value is escaped per the `WWW-Authenticate`
quoted-string grammar (RFC 9110 §11.2 / RFC 7235): a bare `"` or `\`
inside a value would otherwise let an attacker break out of the quotes
and inject additional challenge parameters.

## Configuration callbacks

The transport details are policy a host may override. Each is read from
`AttestoPhoenix.Config` and falls back to the RFC-correct default
implemented here when the host does not set it:

  * `:send_error` - `(conn, status, body_map -> conn)`. Serializes the
    RFC 6749 §5.2 envelope and sends the response. Default encodes JSON
    with `application/json` and halts.
  * `:no_store` - `(conn -> conn)`. Sets the RFC 6749 §5.1 cache
    headers. Default sets `Cache-Control: no-store` and
    `Pragma: no-cache`.
  * `:www_authenticate` - `(conn, challenge_string -> conn)`. Writes the
    challenge header. Default sets the `www-authenticate` response
    header.

The RFC semantics (which code maps to which status, which auth-params,
which header) are owned by this module and are not overridable; only the
serialization/transport is.

This module compiles only when `Plug` is available.

# `scheme`

```elixir
@type scheme() :: :bearer | :dpop
```

The protected-resource authentication scheme a challenge names.

# `t`

```elixir
@type t() :: %AttestoPhoenix.OAuthError{
  error: atom(),
  error_description: String.t() | nil,
  headers: [{String.t(), String.t()}],
  status: pos_integer()
}
```

An OAuth 2.0 error value rendered to the RFC 6749 §5.2 envelope.

# `insufficient_scope`

```elixir
@spec insufficient_scope(Plug.Conn.t(), [String.t()], scheme()) :: Plug.Conn.t()
```

Respond 403 `insufficient_scope` naming the `required` scopes
(RFC 6750 §3.1).

The `WWW-Authenticate` challenge for `scheme` carries the `error`,
`error_description`, and the RFC 6750 §3.1 `scope` auth-param listing the
scopes the request would need. Applies the RFC 6749 §5.1 no-store headers.

# `new`

```elixir
@spec new(atom(), String.t() | nil, keyword()) :: t()
```

Build an OAuth 2.0 error value (RFC 6749 §5.2).

`code` is the error code atom (e.g. `:invalid_request`, `:invalid_client`).
`description` is the human-readable `error_description` (or `nil`). The
HTTP status defaults from the RFC 6749 §5.2 mapping for `code` and can be
overridden with the `:status` option. The `:headers` option carries extra
response headers a caller must emit alongside the error (e.g. the
RFC 9449 §8 `DPoP-Nonce` header on a `use_dpop_nonce` error); it defaults
to `[]`.

# `no_store`

```elixir
@spec no_store(Plug.Conn.t(), AttestoPhoenix.Config.t() | nil) :: Plug.Conn.t()
```

Apply the RFC 6749 §5.1 cache-suppression headers to `conn`.

Sets `Cache-Control: no-store` and `Pragma: no-cache`. Mandatory on every
response that carries an access or refresh token. Delegates to the host's
`:no_store` callback when configured.

# `render`

```elixir
@spec render(Plug.Conn.t(), t()) :: Plug.Conn.t()
```

Render an `%AttestoPhoenix.OAuthError{}` to the RFC 6749 §5.2 wire format.

Writes the JSON envelope `{"error": code, "error_description": desc}` with
the error's status, applies the RFC 6749 §5.1 no-store headers, and - when
the request attempted `Authorization`-based client authentication and the
status is 401 - adds the RFC 6749 §5.2 `WWW-Authenticate: Basic`
challenge. The Basic realm defaults to `"OAuth"` and may be overridden by
the `:basic_realm` config key.

# `unauthorized`

```elixir
@spec unauthorized(Plug.Conn.t(), scheme(), String.t(), keyword()) :: Plug.Conn.t()
```

Respond 401 with a protected-resource `WWW-Authenticate` challenge for
`scheme` (RFC 6750 §3 / RFC 9449 §7.1).

The challenge carries `error` (an OAuth error code string) and, when
supplied, the optional auth-params. Options:

  * `:description` - the `error_description` auth-param.
  * `:scope` - a space-delimited scope string for the `scope` auth-param.
  * `:algs` - a space-delimited list of acceptable DPoP signing
    algorithms for the RFC 9449 §5.1 `algs` auth-param (`:dpop` scheme).
  * `:dpop_nonce` - sets the RFC 9449 §8 `DPoP-Nonce` response header.

Sets the status, the challenge header, any DPoP nonce header, the
RFC 6749 §5.1 no-store headers, and writes the RFC 6749 §5.2 body.

# `use_dpop_nonce`

```elixir
@spec use_dpop_nonce(Plug.Conn.t(), String.t(), keyword()) :: Plug.Conn.t()
```

Respond 401 `use_dpop_nonce` carrying a fresh `DPoP-Nonce` header
(RFC 9449 §8 / §9).

The protected resource (or token endpoint) uses this to demand the client
retry the request including the server-issued `nonce`. Emits a `DPoP`
challenge whose `error` is `use_dpop_nonce`, sets the `DPoP-Nonce`
response header, and applies the RFC 6749 §5.1 no-store headers.

# `www_authenticate`

```elixir
@spec www_authenticate(Plug.Conn.t(), AttestoPhoenix.Config.t() | nil, String.t()) ::
  Plug.Conn.t()
```

Set the `WWW-Authenticate` response header to `challenge`.

Delegates to the host's `:www_authenticate` callback when configured;
otherwise sets the `www-authenticate` header directly.

---

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