Anubis.Server.Authorization (anubis_mcp v1.6.1)

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

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.

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()}
}

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.

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"