Authorization

Copy Markdown

Anubis supports OAuth 2.1 bearer token authorization for HTTP-based transports (:streamable_http and :sse). STDIO transport is exempt per the MCP specification.

Quick Start

defmodule MyApp.MCPServer do
  use Anubis.Server,
    transport: :streamable_http,
    authorization: [
      authorization_servers: ["https://auth.example.com"],
      resource: "https://api.example.com",
      scopes_supported: ["tools:read", "tools:write"],
      validator: {Anubis.Server.Authorization.JWTValidator,
        jwks_uri: "https://auth.example.com/.well-known/jwks.json"}
    ]
end

Every request to the server must include a valid bearer token:

Authorization: Bearer <token>

Requests without a token or with an invalid token receive a 401 Unauthorized response with a WWW-Authenticate header pointing to the protected resource metadata document.

Validators

JWT Validator

Validates signed JWTs by fetching the authorization server's JWKS. Requires the optional :jose dependency:

{:jose, "~> 1.11"}
validator: {Anubis.Server.Authorization.JWTValidator,
  jwks_uri: "https://auth.example.com/.well-known/jwks.json",
  issuer: "https://auth.example.com"   # optional iss validation
}

JWKS responses are cached in :persistent_term for 5 minutes per jwks_uri.

Introspection Validator

Validates tokens via RFC 7662 introspection. Works with any token format:

validator: {Anubis.Server.Authorization.IntrospectionValidator,
  introspection_endpoint: "https://auth.example.com/introspect",
  client_id: "my-resource-server",       # optional Basic auth
  client_secret: "my-secret"
}

Custom Validator

Implement Anubis.Server.Authorization.Validator for any other token format:

defmodule MyApp.TokenValidator do
  @behaviour Anubis.Server.Authorization.Validator

  @impl true
  def validate_token(token, _config) do
    case MyApp.Token.verify(token) do
      {:ok, claims} -> {:ok, claims}
      {:error, reason} -> {:error, reason}
    end
  end
end

Scope Enforcement

Declare required scopes on individual components:

defmodule MyApp.WriteFileTool do
  use Anubis.Server.Component, type: :tool, scopes: ["files:write"]

  schema do
    field :path, :string, required: true
    field :content, :string, required: true
  end

  def execute(params, frame) do
    # Only reached when caller has "files:write" scope
    {:reply, Response.text(Response.tool(), "Written"), frame}
  end
end

Callers missing required scopes receive an MCP execution error indicating insufficient_scope with the required and granted scopes in the error payload.

Accessing Claims in Handlers

Validated claims are available on the frame:

def execute(params, frame) do
  subject = Anubis.Server.Frame.subject(frame)      # "user-123"
  scopes  = Anubis.Server.Frame.scopes(frame)       # ["tools:read", "tools:write"]
  auth    = Anubis.Server.Frame.authorization(frame) # full claims map

  if Anubis.Server.Frame.has_scope?(frame, "admin") do
    # privileged path
  end

  {:reply, Response.text(Response.tool(), "Hello #{subject}"), frame}
end

Protected Resource Metadata

The server serves the RFC 9728 metadata document at:

GET /.well-known/oauth-protected-resource

Response:

{
  "resource": "https://api.example.com",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["tools:read", "tools:write"],
  "bearer_methods_supported": ["header"]
}

The SSE and Streamable HTTP plugs handle this path inline when they are mounted at the root of the host. If you mount the MCP plug under a sub-path (e.g. /sse, /mcp), requests to /.well-known/oauth-protected-resource never reach the plug. In that case mount Anubis.Server.Transport.WellKnown as a sibling route:

# Plug.Router
forward "/.well-known/oauth-protected-resource",
  to: Anubis.Server.Transport.WellKnown,
  init_opts: [server: MyApp.MCPServer]

forward "/sse", to: Anubis.Server.Transport.SSE.Plug,
  init_opts: [server: MyApp.MCPServer, mode: :sse]
# Phoenix
forward "/.well-known/oauth-protected-resource",
  Anubis.Server.Transport.WellKnown,
  server: MyApp.MCPServer

Standards

StandardCoverage
RFC 6750Bearer token usage on Authorization header
RFC 9728Protected Resource Metadata (/.well-known/oauth-protected-resource)
RFC 8707Audience validation (aud claim against resource URI)
RFC 7662Token Introspection
RFC 7519 + 7517JWT + JWKS verification

Client-side OAuth flows (PKCE, discovery, token store, refresh) are out of scope. Use a dedicated OAuth client library for those.