Skip to main content

lib/noizu/mcp/auth/token_verifier.ex

defmodule Noizu.MCP.Auth.TokenVerifier do
  @moduledoc """
  Server-side bearer-token verification for the Streamable HTTP transport.

  MCP servers are OAuth 2.1 *resource servers*: they validate access tokens
  but never issue them. Implement this behaviour and pass it to the transport
  plug:

      forward "/mcp", Noizu.MCP.Transport.StreamableHTTP.Plug,
        server: MyApp.MCP,
        auth: [
          verifier: {MyApp.TokenVerifier, []},
          resource_metadata: "https://api.example.com/.well-known/oauth-protected-resource"
        ]

      defmodule MyApp.TokenVerifier do
        @behaviour Noizu.MCP.Auth.TokenVerifier

        @impl true
        def verify(token, _conn_info, _opts) do
          with {:ok, claims} <- MyApp.JWT.verify(token),
               # RFC 8707: the token must be audience-bound to THIS server.
               true <- claims["aud"] == "https://api.example.com/mcp" || {:error, :invalid_token} do
            {:ok, claims}
          end
        end
      end

  Verified claims are exposed to handlers as `ctx.assigns.auth_claims`
  (seeded at session initialize).
  """

  @type conn_info :: %{method: String.t(), peer: term(), headers: [{binary(), binary()}]}

  @doc """
  Verify a bearer token. Return `{:ok, claims}`, `{:error, :invalid_token}`
  (→ 401), or `{:error, :insufficient_scope, meta}` (→ 403 with an
  `insufficient_scope` challenge; `meta` may include `"scope"`).

  Audience validation (RFC 8707) is the verifier's responsibility — reject
  tokens minted for other resources.
  """
  @callback verify(token :: String.t(), conn_info(), opts :: term()) ::
              {:ok, claims :: map()}
              | {:error, :invalid_token}
              | {:error, :insufficient_scope, meta :: map()}
end