# `Urchin.Auth`
[🔗](https://github.com/urth-inc/urchin/blob/v0.4.0/lib/urchin/auth.ex#L1)

OAuth 2.1 Resource Server configuration and logic for the MCP authorization spec
(revision 2025-11-25).

Urchin acts purely as an OAuth 2.1 **Resource Server (RS)**: it delegates inbound
access-token decisions to an injected authorizer and advertises the location of its
Authorization Server(s) through RFC 9728 Protected Resource Metadata. The Authorization
Server - the token, authorization and registration endpoints, PKCE, consent - is out of
scope and may be any external entity.

Authorization is **optional and off by default**. A transport mounted without `:auth`
serves MCP unauthenticated, exactly as before. Pass an `Urchin.Auth` (or a keyword
list coerced into one) to turn it on:

    auth =
      Urchin.Auth.new!(
        resource: "https://mcp.example.com/mcp",
        authorization_servers: ["https://auth.example.com"],
        scopes_supported: ["mcp:tools", "files:read", "files:write"],
        authorizer: &MyApp.Auth.authorize/3
      )

    # one-call runner (also serves the well-known metadata endpoint):
    Urchin.start_link(MyServer, port: 4000, path: "/mcp", auth: auth)

    # or mounted as a Plug pipeline:
    plug Urchin.Auth.Metadata, auth: auth
    plug Urchin.Auth.Plug, auth: auth
    forward "/mcp", to: Urchin.Transport.StreamableHTTP, init_opts: [server: MyServer]

This module is the single source of truth: it builds the metadata document and the
`WWW-Authenticate` challenges, and it invokes an injected authorizer. Request context is
passed through as an opaque `conn` term so tenant/realm-aware callbacks can resolve
per-request authorization data.

## Options (`new!/1`)

  * `:resource` (required) - the canonical server URI, e.g.
    `"https://mcp.example.com/mcp"`. Used as the metadata `resource` field. The
    configured authorizer should enforce this as the token audience/resource binding when
    RFC 8707 applies. MUST be absolute and MUST NOT carry a fragment.
  * `:authorization_servers` (required) - a non-empty list of AS issuer URLs, or a
    1-arity function `fn conn -> [issuer] end`, surfaced in the metadata document.
  * `:authorizer` (required) - a module implementing `Urchin.Auth.Authorizer`, or a
    3-arity function `fn token, auth, conn -> result end`. It owns the full authorization
    decision: token validity, issuer, expiry, audience, scopes and tenant policy.
  * `:scopes_supported` - optional list of scopes advertised in the metadata document.
  * `:required_scopes` - scopes every request should carry. A list, or a 1-arity function
    `fn conn -> [scope] end` for per-request requirements. Urchin uses this for challenge
    hints; the authorizer decides whether and how to enforce it. Default `[]`.
  * `:bearer_methods_supported` - default `["header"]` (MCP requires header tokens).
  * `:resource_name`, `:jwks_uri`, `:resource_documentation` - optional metadata fields.
  * `:resource_metadata_url` - optional absolute URL, or `fn conn -> url end`, used in
    `WWW-Authenticate: resource_metadata`. Defaults to the RFC 9728 well-known URL for
    `:resource`. Use a resolver to preserve tenant context in the challenge, e.g. by adding
    a query parameter (`?realm=...`). The metadata document is served only at the static
    well-known paths derived from `:resource`, so a resolver that points at a different path
    (e.g. a per-tenant path segment) must be served by your own route or an external host.
  * `:metadata` - a map of extra RFC 9728 fields. Reserved fields managed by Urchin
    cannot be overridden.
  * `:allow_insecure_authorization_servers` - permit non-HTTPS issuer URLs (localhost is
    always allowed). Default `false`.

# `authorization_servers`

```elixir
@type authorization_servers() :: [String.t()] | (term() -&gt; [String.t()])
```

# `authorizer`

```elixir
@type authorizer() ::
  module()
  | (token :: String.t() | nil, auth :: t(), conn :: term() -&gt;
       Urchin.Auth.Authorizer.result())
```

# `kind`

```elixir
@type kind() ::
  :missing
  | :invalid_token
  | :insufficient_scope
  | :invalid_request
  | :server_error
```

# `resource_metadata_url`

```elixir
@type resource_metadata_url() :: String.t() | (term() -&gt; String.t())
```

# `t`

```elixir
@type t() :: %Urchin.Auth{
  allow_insecure_authorization_servers: boolean(),
  authorization_servers: authorization_servers(),
  authorizer: {:module, module()} | {:fun, fun()},
  bearer_methods_supported: [String.t()],
  jwks_uri: String.t() | nil,
  metadata: map(),
  required_scopes: [String.t()] | (term() -&gt; [String.t()]),
  resource: String.t(),
  resource_documentation: String.t() | nil,
  resource_metadata_url: resource_metadata_url(),
  resource_name: String.t() | nil,
  resource_uri: URI.t(),
  scopes_supported: [String.t()] | nil,
  well_known_paths: [String.t()]
}
```

# `authorization_servers`

```elixir
@spec authorization_servers(t(), term()) :: [String.t()]
```

Resolves the configured Authorization Server issuer URLs for the request.

# `authorize`

```elixir
@spec authorize(t(), String.t() | nil, term()) ::
  {:ok, Urchin.Auth.Claims.t()} | {:error, kind(), String.t()}
```

Authorizes a bearer token (or its absence) against this configuration.

Delegates the final decision to the configured authorizer. Returns `{:ok, claims}` or
`{:error, kind, message}`, where `kind` selects the challenge (see `challenge/5`).

# `challenge`

```elixir
@spec challenge(t(), kind(), String.t(), term()) ::
  {100..599, String.t() | nil, map()}
```

Builds the HTTP response for a failed authorization decision.

Returns `{status, www_authenticate, body}` where `www_authenticate` is the header
string (or `nil` for 500) and `body` is the OAuth 2.0 error object. The scope hint and
`resource_metadata` URL are resolved here from the (possibly per-request) configuration;
a resolver that raises degrades the header to a minimal challenge rather than escalating
the response to a 500 with no `WWW-Authenticate`.

# `coerce!`

```elixir
@spec coerce!(t() | keyword() | map() | nil) :: t() | nil
```

Coerces a transport/plug `:auth` option into an `Urchin.Auth` (or `nil` when disabled).

Accepts an existing struct, a keyword list / map of options, or `nil`.

# `metadata_document`

```elixir
@spec metadata_document(t(), term()) :: map()
```

Returns the RFC 9728 Protected Resource Metadata document as a JSON-encodable map.

# `new`

```elixir
@spec new(keyword() | map()) :: {:ok, t()} | {:error, String.t()}
```

Like `new!/1`, but returns `{:ok, auth}` or `{:error, message}`.

# `new!`

```elixir
@spec new!(keyword() | map()) :: t()
```

Builds an `Urchin.Auth` from options, raising `ArgumentError` on invalid input.

See the module documentation for the option list.

# `required_scopes`

```elixir
@spec required_scopes(t(), term()) :: [String.t()]
```

Resolves the scopes required for a request (static list or `fn conn -> [...] end`).

# `resource_metadata_url`

```elixir
@spec resource_metadata_url(t(), term()) :: String.t()
```

The absolute URL of the Protected Resource Metadata document (for `resource_metadata`).

# `well_known_paths`

```elixir
@spec well_known_paths(t(), term()) :: [String.t()]
```

The request paths at which the metadata document is served (canonical + root).

---

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