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:
- the
resource_metadataURL from theWWW-Authenticatechallenge, when given; https://ex.com/.well-known/oauth-protected-resource/public/mcp;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:
https://auth.ex.com/.well-known/oauth-authorization-server/tenant1https://auth.ex.com/.well-known/openid-configuration/tenant1https://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):
issuerto equal the issuer the URLs were built from, by exact string comparison (RFC 8414 §3.3):https://AS.exampleis nothttps://as.example, and a trailing slash matters;"S256"incode_challenge_methods_supported;authorization_endpointandtoken_endpointto be present;- every endpoint - each top-level field ending in
_endpoint, plusjwks_uri- to be anhttpsURL with a host, no user info and no fragment.httpsis the whole scheme allowlist, sojavascript:,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.
Summary
Functions
The authorization-server metadata URLs to try for issuer, in order.
The Protected Resource Metadata URLs to try for server_url, in order.
Chooses the authorization server from validated PRM: the first entry of
authorization_servers.
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.
Validates decoded Protected Resource Metadata against the canonical server
URI (from ConduitMcp.Client.OAuth.Resource.canonical/2) and returns it
unchanged.
Functions
@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"
]}
@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"
]}
@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.
@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- admithttpendpoints 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_credentialsneither"S256"norauthorization_endpointis required;token_endpointstill is, and every endpoint present is still checked.
@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.