# `AttestoMCP.Plug.ProtectResource`
[🔗](https://github.com/XukuLLC/attesto_mcp/blob/v1.0.5/lib/attesto_mcp/plug/protect_resource.ex#L1)

Protect an HTTP MCP endpoint in one plug.

The MCP authorization spec treats a protected HTTP MCP server as an OAuth
resource server (RFC 9728). Guarding such an endpoint correctly takes two
ordered steps: authenticate the access token (and any DPoP/mTLS sender
constraint), then enforce the scopes the route requires. `ProtectResource`
composes `AttestoMCP.Plug.Authenticate` followed by
`AttestoMCP.Plug.RequireScopes` into a single, correctly ordered,
halt-respecting pipeline so the host does not hand-wire and re-order the two
plugs (and the WWW-Authenticate `resource_metadata` challenge) on every
route.

    plug AttestoMCP.Plug.ProtectResource,
      config: &MyApp.Attesto.config/0,
      replay_check: &MyApp.DPoPReplay.check_and_record/2,
      resource: "/mcp",
      scopes: [AttestoMCP.Scopes.tools_call()]

`init/1` returns no closures, so the plug works as a compile-time router
pipeline plug (`plug_init_mode: :compile`, the production default). As Plug
requires under `:compile` mode, any callback the host passes (`:config`,
`:replay_check`, `:send_error`, …) must be a remote capture (`&Mod.fun/n`) or
an MFA tuple (`{Mod, :fun}`), not an anonymous `fn`.

This is exactly equivalent to:

    plug AttestoMCP.Plug.Authenticate,
      config: &MyApp.Attesto.config/0,
      replay_check: &MyApp.DPoPReplay.check_and_record/2,
      resource_path: "/mcp"

    plug AttestoMCP.Plug.RequireScopes,
      scopes: [AttestoMCP.Scopes.tools_call()]

## Options

  * `:scopes` (or `:scope`) - the scope(s) the route requires, forwarded to
    `AttestoMCP.Plug.RequireScopes`. At least one scope is required.
  * `:step_up` - an optional RFC 9470 step-up requirement for the route
    (`[acr_values: ["phr"], max_age: 300]` or an
    `%Attesto.StepUp.Requirement{}`). After the token is verified, its
    `acr` / `auth_time` claims must satisfy the requirement or the request is
    refused 401 `insufficient_user_authentication`, naming the `acr_values` /
    `max_age` the client must re-request at the authorization endpoint. A
    token from a machine grant (no `auth_time`) always challenges a freshness
    requirement, so step-up routes are for end-user grants.
  * `:resource` (or `:resource_path`) - the MCP endpoint path, for example
    `"/mcp"` or `"/mcp/brokers"`. It drives the RFC 9728 `resource_metadata`
    auth-param appended to `WWW-Authenticate` challenges, derived from the
    live request origin via `AttestoMCP.Metadata.protected_resource_url/2`.
    Both names mean the same thing; `:resource` reads naturally here while
    `:resource_path` matches `AttestoMCP.Plug.Authenticate`.
  * `:base_url` (or `:origin`) - pin the origin of the `resource_metadata`
    challenge URL behind a TLS-terminating proxy (a `String.t()` or
    `(conn -> url)`), instead of trusting the proxy-rewritten request. When
    omitted, the live request origin is used. This keeps the challenge URL
    aligned with a pinned metadata `resource` and closes the
    `X-Forwarded-Host` spoofing vector. See `guides/proxy_origin.md`.
  * `:resource_audience` - confines access tokens to this protected resource;
    `:resource` derives its audience identifier from the resource path and
    resolved origin. A scalar token audience must match, and every member of
    an array-valued audience must match. This option is mutually exclusive
    with Attesto core's `:trusted_audiences`; configuring both raises.

Every other option is passed through to `AttestoMCP.Plug.Authenticate`:
`:config`, `:replay_check`, `:nonce_check`, `:nonce_issue`, `:cert_der`,
`:trusted_audiences`, `:htu`, `:credential_from_conn`, `:bearer_methods`, `:send_error`,
`:www_authenticate`, `:no_store`, `:principal`, `:principal_key`,
`:claims_key`, `:scopes_key`, `:sender_key`, and `:resource_metadata_url`.
MCP defaults to `bearer_methods: [:header]`; set
`bearer_methods: [:header, :body]` only when the resource intentionally
accepts RFC 6750 §2.2 form-body access tokens. The transport hooks
(`:send_error`, `:www_authenticate`, `:no_store`) and the assign keys
(`:claims_key`, `:scopes_key`) are also shared with
`AttestoMCP.Plug.RequireScopes` so a scope rejection renders through the same
host-controlled error envelope.

---

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