Authorization
Copy MarkdownAnubis 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"}
]
endEvery 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
endScope 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
endCallers 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}
endProtected Resource Metadata
The server serves the RFC 9728 metadata document at:
GET /.well-known/oauth-protected-resourceResponse:
{
"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.MCPServerStandards
| Standard | Coverage |
|---|---|
| RFC 6750 | Bearer token usage on Authorization header |
| RFC 9728 | Protected Resource Metadata (/.well-known/oauth-protected-resource) |
| RFC 8707 | Audience validation (aud claim against resource URI) |
| RFC 7662 | Token Introspection |
| RFC 7519 + 7517 | JWT + JWKS verification |
Client-side OAuth flows (PKCE, discovery, token store, refresh) are out of scope. Use a dedicated OAuth client library for those.