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

Pure parts of authorization-server discovery (MCP authorization spec,
*Authorization Server Discovery*): the metadata URLs to try, in order, and
validation of the decoded metadata documents. Nothing here does I/O; the
HTTP layer fetches each candidate (without an `Authorization` header, with
every URL passing `ConduitMcp.Client.URLPolicy` `:public_only`) and stops
at the first usable document.

## Protected Resource Metadata (RFC 9728)

`prm_candidates/3` for server `https://ex.com/public/mcp`:

  1. the `resource_metadata` URL from the `WWW-Authenticate` challenge, when given;
  2. `https://ex.com/.well-known/oauth-protected-resource/public/mcp`;
  3. `https://ex.com/.well-known/oauth-protected-resource`.

`validate_prm/2` then requires `resource` to equal the canonical server URI
**exactly** (RFC 9728 §3.3; no prefix or same-origin relaxation, OA-4) and
a non-empty `authorization_servers`; `select_issuer/2` picks the first one.

## Authorization-server metadata (RFC 8414, OpenID Connect Discovery)

`as_metadata_candidates/2` for issuer `https://auth.ex.com/tenant1`:

  1. `https://auth.ex.com/.well-known/oauth-authorization-server/tenant1`
  2. `https://auth.ex.com/.well-known/openid-configuration/tenant1`
  3. `https://auth.ex.com/tenant1/.well-known/openid-configuration`

and for an issuer without a path, `/.well-known/oauth-authorization-server`
then `/.well-known/openid-configuration`.

`validate_as_metadata/3` then requires (OA-1, OA-4, OA-7):

  * `issuer` to equal the issuer the URLs were built from, by exact string
    comparison (RFC 8414 §3.3): `https://AS.example` is not
    `https://as.example`, and a trailing slash matters;
  * `"S256"` in `code_challenge_methods_supported`;
  * `authorization_endpoint` and `token_endpoint` to be present;
  * every endpoint - each top-level field ending in `_endpoint`, plus
    `jwks_uri` - to be an `https` URL with a host, no user info and no
    fragment. `https` is the whole scheme allowlist, so `javascript:`,
    `file:`, `data:` and custom schemes are refused as `:insecure_endpoint`.

Every builder and validator accepts `allow_insecure: true`, which admits
`http` on loopback hosts only, for local development and tests.

# `as_metadata_candidates`

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

The authorization-server metadata URLs to try for `issuer`, in order.

The issuer must be an `https` URL without a query or fragment (RFC 8414
§2). A terminating slash is removed before the well-known path is
inserted (RFC 8414 §3.1), so `https://as.example/` has no path.

    iex> ConduitMcp.Client.OAuth.Discovery.as_metadata_candidates("https://auth.ex.com")
    {:ok,
     [
       "https://auth.ex.com/.well-known/oauth-authorization-server",
       "https://auth.ex.com/.well-known/openid-configuration"
     ]}

# `prm_candidates`

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

The Protected Resource Metadata URLs to try for `server_url`, in order.

`resource_metadata` (from `ConduitMcp.Client.OAuth.Challenge.resource_metadata/1`)
goes first, unchanged; it is server data and must pass the URL policy when
fetched. The well-known URLs are built from the canonical server URI
(`ConduitMcp.Client.OAuth.Resource.canonical/2`), keeping any query after
the inserted path. Duplicates are removed.

    iex> ConduitMcp.Client.OAuth.Discovery.prm_candidates("https://ex.com/public/mcp")
    {:ok,
     [
       "https://ex.com/.well-known/oauth-protected-resource/public/mcp",
       "https://ex.com/.well-known/oauth-protected-resource"
     ]}

# `select_issuer`

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

Chooses the authorization server from validated PRM: the first entry of
`authorization_servers`.

## Options

  * `:expected_issuers` - when a list, the chosen issuer must be in it
    (exact string match), else `:issuer_not_allowed`. `nil` (default)
    accepts any.

# `validate_as_metadata`

```elixir
@spec validate_as_metadata(term(), String.t(), keyword()) ::
  {:ok, map()} | {:error, ConduitMcp.Client.OAuth.Error.t()}
```

Validates decoded authorization-server metadata fetched for `issuer` and
returns it unchanged. See the moduledoc for the rules; they are checked in
this order: object, `issuer`, S256, required endpoints, every endpoint.

Errors: `:invalid_metadata` (data `%{field: name}`), `:issuer_mismatch`,
`:pkce_unsupported`, `:insecure_endpoint` (data `%{field: name, scheme:
scheme}`).

## Options

  * `:allow_insecure` - admit `http` endpoints on loopback hosts.
  * `:grant` - `:authorization_code` (default) or `:client_credentials`.
    A machine-to-machine client never sends the user to the
    authorization endpoint, so for `:client_credentials` neither `"S256"`
    nor `authorization_endpoint` is required; `token_endpoint` still is,
    and every endpoint present is still checked.

# `validate_prm`

```elixir
@spec validate_prm(term(), String.t()) ::
  {:ok, map()} | {:error, ConduitMcp.Client.OAuth.Error.t()}
```

Validates decoded Protected Resource Metadata against the canonical server
URI (from `ConduitMcp.Client.OAuth.Resource.canonical/2`) and returns it
unchanged.

Errors: `:invalid_metadata` (not an object; `authorization_servers` or
`scopes_supported` not a list of strings; data names the field),
`:resource_mismatch`, `:no_authorization_server`.

---

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