Anubis.Server.Authorization (anubis_mcp v2.2.0)

Copy Markdown

OAuth 2.1 resource server authorization support.

Provides configuration, metadata building, and token validation primitives for securing MCP servers with bearer token authorization.

Standards Implemented

  • RFC 6750 — Bearer Token Usage
  • RFC 9728 — Protected Resource Metadata
  • RFC 8707 — Resource Indicators (audience validation)
  • RFC 7662 — Token Introspection
  • RFC 7519 — JSON Web Token (JWT)

Configuration

use MyServer,
  authorization: [
    authorization_servers: ["https://auth.example.com"],
    resource: "https://api.example.com",
    realm: "mcp",
    scopes_supported: ["tools:read", "tools:write"],
    validator: {Anubis.Server.Authorization.JWTValidator,
      jwks_uri: "https://auth.example.com/.well-known/jwks.json"}
  ]

Claims map

After successful validation, a normalized claims map is stored in Context.auth:

%{
  sub: "user-id",
  aud: "https://api.example.com",
  scope: "tools:read tools:write",
  scopes: ["tools:read", "tools:write"],
  exp: 1_234_567_890,
  iat: 1_234_567_800,
  client_id: "client-abc",
  raw_claims: %{}
}

Summary

Types

Bounded authorization outcomes carried in telemetry :stop metadata.

Functions

Builds the RFC 9728 protected resource metadata map.

Builds the WWW-Authenticate header value for a 401 unauthorized response.

Normalizes raw claims (string-keyed map) into the canonical claims shape.

Maps an authorization or validator error reason to a bounded telemetry outcome.

Parses and validates the authorization configuration keyword list.

Validates that the token aud claim matches the server's canonical resource URI.

Validates that the token has not expired.

Validates that the claims contain all required scopes.

Returns the canonical /.well-known/oauth-protected-resource URL for a resource URI.

Types

claims()

@type claims() :: %{
  sub: String.t() | nil,
  aud: String.t() | [String.t()] | nil,
  scope: String.t() | nil,
  scopes: [String.t()],
  exp: integer() | nil,
  iat: integer() | nil,
  client_id: String.t() | nil,
  raw_claims: map()
}

config()

@type config() :: %{
  authorization_servers: [String.t()],
  resource: String.t(),
  realm: String.t(),
  scopes_supported: [String.t()],
  validator: {module(), keyword()}
}

outcome()

@type outcome() ::
  :ok
  | :missing_token
  | :invalid_token
  | :invalid_issuer
  | :invalid_audience
  | :token_expired
  | :invalid_expiry
  | :validator_unavailable
  | :other

Bounded authorization outcomes carried in telemetry :stop metadata.

Functions

build_resource_metadata(config)

@spec build_resource_metadata(config()) :: map()

Builds the RFC 9728 protected resource metadata map.

Examples

Authorization.build_resource_metadata(config)
# => %{
#      "resource" => "https://api.example.com",
#      "authorization_servers" => ["https://auth.example.com"],
#      "scopes_supported" => ["tools:read"],
#      "bearer_methods_supported" => ["header"]
#    }

build_www_authenticate(config, arg2)

@spec build_www_authenticate(
  config(),
  :unauthorized | {:insufficient_scope, String.t()}
) :: String.t()

Builds the WWW-Authenticate header value for a 401 unauthorized response.

Includes resource_metadata URL per RFC 9728.

Examples

Authorization.build_www_authenticate(config, :unauthorized)
# => ~s(Bearer realm="mcp", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource")

get_schema(atom)

normalize_claims(raw)

@spec normalize_claims(map()) :: claims()

Normalizes raw claims (string-keyed map) into the canonical claims shape.

Parses the scope string into a scopes list for convenient membership checks. If the raw claims already contain a scopes list (string- or atom-keyed), it is preserved as-is so custom validators that emit pre-normalized data are honored.

outcome(arg1)

@spec outcome(term()) :: outcome()

Maps an authorization or validator error reason to a bounded telemetry outcome.

The Streamable HTTP plug wraps bearer authorization in a telemetry span whose :stop metadata carries an outcome drawn from the bounded set in outcome/0. Validator error reasons are free-form (atoms, strings, tuples), so they are normalized here; raw reasons, tokens, and claims never reach telemetry metadata.

Validator failures to reach or parse the token source (JWKS and introspection fetches) map to :validator_unavailable; token verification and issuer failures map to their specific outcomes; anything unrecognized maps to :other.

Examples

Authorization.outcome(:token_expired)
# => :token_expired

Authorization.outcome(:invalid_signature)
# => :invalid_token

Authorization.outcome({:jwks_request_failed, :nxdomain})
# => :validator_unavailable

Authorization.outcome("unrecognized")
# => :other

parse_config!(opts)

@spec parse_config!(keyword()) :: config()

Parses and validates the authorization configuration keyword list.

Raises ArgumentError if required fields are missing or invalid.

Examples

config = Authorization.parse_config!(
  authorization_servers: ["https://auth.example.com"],
  resource: "https://api.example.com",
  validator: {MyValidator, []}
)

parse_config_schema(data)

parse_config_schema!(data)

validate_audience(arg1, config)

@spec validate_audience(claims(), config()) :: :ok | {:error, :invalid_audience}

Validates that the token aud claim matches the server's canonical resource URI.

Returns :ok when the audience matches, {:error, :invalid_audience} otherwise.

Examples

Authorization.validate_audience(%{aud: "https://api.example.com"}, config)
# => :ok

Authorization.validate_audience(%{aud: "https://other.example.com"}, config)
# => {:error, :invalid_audience}

validate_expiry(arg1)

@spec validate_expiry(claims()) :: :ok | {:error, :token_expired | :invalid_expiry}

Validates that the token has not expired.

Compares exp against the current Unix timestamp. Returns :ok if not expired, {:error, :token_expired} otherwise. Tokens without exp are treated as non-expiring.

Examples

Authorization.validate_expiry(%{exp: future_timestamp})
# => :ok

validate_scopes(arg1, required)

@spec validate_scopes(claims(), [String.t()]) ::
  :ok | {:error, {:insufficient_scope, [String.t()]}}

Validates that the claims contain all required scopes.

Returns :ok when all required scopes are present in the claims, {:error, {:insufficient_scope, required_scopes}} otherwise.

Examples

Authorization.validate_scopes(%{scopes: ["tools:read", "tools:write"]}, ["tools:read"])
# => :ok

Authorization.validate_scopes(%{scopes: ["tools:read"]}, ["tools:write"])
# => {:error, {:insufficient_scope, ["tools:write"]}}

well_known_url(resource)

@spec well_known_url(String.t()) :: String.t()

Returns the canonical /.well-known/oauth-protected-resource URL for a resource URI.

Examples

Authorization.well_known_url("https://api.example.com")
# => "https://api.example.com/.well-known/oauth-protected-resource"