# `ConduitMcp.Client.OAuth.Challenge`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/oauth/challenge.ex#L1)

Parses `WWW-Authenticate` header values (RFC 9110 §11.6.1, RFC 6750 §3) and
reads the Bearer challenge an MCP server sends with a 401 or 403.

    iex> {:ok, [challenge]} =
    ...>   ConduitMcp.Client.OAuth.Challenge.parse(
    ...>     ~s(Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read files:write")
    ...>   )
    iex> ConduitMcp.Client.OAuth.Challenge.scopes(challenge)
    ["files:read", "files:write"]

## Grammar

    WWW-Authenticate = #challenge
    challenge        = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
    auth-param       = token BWS "=" BWS ( token / quoted-string )

The parser is a hand-written recursive descent over the bytes:

  * One header value may hold several challenges, and `parse/1` also takes
    a list of values (one per header line); the challenges are returned in
    order. Empty list elements (`, ,`) are skipped (RFC 9110 §5.6.1).
  * A comma separates both challenges and parameters. After a comma, the
    next element is a parameter when it starts with `token BWS "="`, and a
    new challenge otherwise.
  * After the scheme, a run of token68 characters (optionally ending in
    `=`s) followed by the end of the value or a comma is a token68
    (`Basic dXNlcjpwYXNz`, and also `Bearer realm=` - a parameter needs a
    non-empty value, so that reads as token68 `realm=`).
  * Quoted strings honour `\` escapes (quoted-pair); commas, `=` and
    spaces inside quotes are data. A bare control character (CR, LF, NUL,
    ...) anywhere is an error, so a header cannot smuggle a line break.
  * Scheme and parameter names are case-insensitive: both are stored
    lowercased (ASCII). Values are kept exactly as sent and must be valid
    UTF-8.

Malformed input never raises: it returns `{:error, %OAuth.Error{reason:
:invalid_challenge, data: detail}}`, and the whole input is rejected (no
partial result), so the caller falls back to well-known discovery.

## Duplicates

RFC 9110 §11.2 says each parameter name "MUST only occur once per
challenge" and RFC 6750 §3 says Bearer attributes "MUST NOT appear more
than once". A repeated name (compared case-insensitively) is rejected with
detail `:duplicate_param` rather than resolved first- or last-wins: two
parties reading the same header must never disagree about, say,
`resource_metadata`.

## Limits

  * 8192 bytes across all values (detail `:too_large`);
  * 16 challenges in total (`:too_many_challenges`);
  * 32 parameters per challenge (`:too_many_params`).

Other details: `:invalid_input` (not a string or list of strings),
`:malformed`, `:unterminated_quote`, `:invalid_encoding`.

# `detail`

```elixir
@type detail() ::
  :invalid_input
  | :too_large
  | :too_many_challenges
  | :too_many_params
  | :duplicate_param
  | :unterminated_quote
  | :invalid_encoding
  | :malformed
```

# `t`

```elixir
@type t() :: %ConduitMcp.Client.OAuth.Challenge{
  params: %{optional(String.t()) =&gt; String.t()},
  scheme: String.t(),
  token68: String.t() | nil
}
```

One challenge. `scheme` and the `params` keys are lowercase; `token68` is
set (and `params` empty) for the token68 form.

# `bearer`

```elixir
@spec bearer([t()]) :: t() | nil
```

The first challenge with the `Bearer` scheme, or `nil`. Takes the list
from `parse/1`; RFC 6750 defines a single Bearer challenge, so any later
ones are ignored.

# `error`

```elixir
@spec error(t()) :: String.t() | nil
```

The RFC 6750 §3.1 `error` code (for example `"insufficient_scope"`), or `nil`.

# `error_description`

```elixir
@spec error_description(t()) :: String.t() | nil
```

The `error_description`, or `nil`. Server text: never render it as trusted.

# `insufficient_scope?`

```elixir
@spec insufficient_scope?(t()) :: boolean()
```

Whether the challenge is a step-up request (`error="insufficient_scope"`).

# `param`

```elixir
@spec param(t(), String.t()) :: String.t() | nil
```

The value of parameter `name` (matched case-insensitively), or `nil`.

# `parse`

```elixir
@spec parse(String.t() | [String.t()]) ::
  {:ok, [t()]} | {:error, ConduitMcp.Client.OAuth.Error.t()}
```

Parses one `WWW-Authenticate` value, or a list of values (one per header).

    iex> ConduitMcp.Client.OAuth.Challenge.parse([~s(Basic realm="a, b"), "Bearer"])
    {:ok,
     [
       %ConduitMcp.Client.OAuth.Challenge{scheme: "basic", params: %{"realm" => "a, b"}},
       %ConduitMcp.Client.OAuth.Challenge{scheme: "bearer", params: %{}}
     ]}

# `resource_metadata`

```elixir
@spec resource_metadata(t()) :: String.t() | nil
```

The `resource_metadata` URL (RFC 9728 §5.1), or `nil`. It is server data:
validate it and pass it through `ConduitMcp.Client.URLPolicy` before
fetching.

# `scopes`

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

The challenged scopes (RFC 6750 §3 `scope`, space-delimited), `[]` when absent.

---

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