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

Validates an authorization response (the query parameters delivered to the
redirect URI) against its `ConduitMcp.Client.OAuth.Pending`, before the
code goes anywhere near a token endpoint.

`validate/3` runs these checks in order and stops at the first failure:

  1. **`state`** equals the pending state, compared in constant time with
     `Plug.Crypto.secure_compare/2`; missing or different ⇒ `:invalid_state`.
  2. **Principal** - the `:principal` the callback is completed under equals
     (`===`) the one the flow was started with ⇒ else `:principal_mismatch`.
  3. **Expiry** - `now` is before `expires_at` ⇒ else `:state_expired`.
  4. **`iss`** (RFC 9207 §2.4, OA-3), by exact string comparison with the
     recorded issuer and no normalization of case, slashes or ports:

     | `iss` in the response | AS advertises `iss` support | Result |
     |---|---|---|
     | present, equal | either | continue |
     | present, different | either | `:iss_mismatch` |
     | absent | `true` | `:iss_missing` |
     | absent | `false` / not advertised | continue |

     A present `iss` is compared even when the authorization server never
     advertised the parameter. A key with a `nil` value counts as absent.
  5. **`error`** - an error response becomes the matching typed reason
     (`:access_denied`, ... or `:authorization_error` for an unknown code),
     with the server's `error`, `error_description` and `error_uri` strings
     in `:data`.
  6. **`code`** - returned wrapped in `ConduitMcp.Client.Secret`; absent ⇒
     `:missing_code`.

Steps 1-4 come first so that a response from the wrong party, or for the
wrong flow, surfaces nothing it carries: on any of those failures the
error's `:data` is `nil`, and in particular a mismatched response's
`error_description` is never passed through.

## Replay

`validate/3` is pure, so it accepts the same `Pending` and parameters as
often as it is called. Single use of `state` is enforced by the host's
store, which must delete the pending entry when it is looked up (see
`ConduitMcp.Client.OAuth.Pending`).

# `validate`

```elixir
@spec validate(ConduitMcp.Client.OAuth.Pending.t(), map(), keyword()) ::
  {:ok, ConduitMcp.Client.Secret.t()}
  | {:error, ConduitMcp.Client.OAuth.Error.t()}
```

Validates `params` (string-keyed, as decoded from the redirect query) for
`pending`. See the moduledoc for the order of checks.

## Options

  * `:principal` - the host identity completing the flow (default `nil`;
    must equal the pending principal).
  * `:now` - current Unix time in seconds (default: the system clock).

---

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