relay/authorization

Bearer-token authorization for an MCP resource server: tokens, the protected resource and its scopes, a verifier, admission, RFC 6750 challenges and RFC 9728 protected-resource metadata.

A Verifier is a function the application supplies: it receives the BearerToken and the request’s correlation and returns an Attestation of the principal, the audiences and the scopes, or a VerificationError. Relay never parses or checks a token itself; the verifier owns signatures, expiry, issuers and any introspection call. admit then accepts the attestation only when it names exactly the protected resource as its one audience and carries every required scope, and returns a Grant.

A verifier also receives the request’s sinal/correlation, the same value the request’s telemetry and its tool handler see, so an introspection call can carry it. The correlation may come from the client and is never evidence of identity: decide only on the token.

relay/http.new_protected wires this module into the HTTP endpoint: it reads the Authorization header, answers a refusal with the challenge status and WWW-Authenticate header, and serves resource_metadata at metadata_path. Use the functions directly for a custom transport.

A token, a scope and a resource are readable (token_value, scope_name, resource_uri), so a verifier can pass the token to a validator. A BearerToken keeps its value inside a closure, so string.inspect, crash reports and logs print a function reference instead of the token.

A local JWT validator plugs in as the verifier. With a validator that returns claims holding subject, audiences and scopes, the verifier is one expression:

import gleam/result
import relay/authorization

pub type Claims {
  Claims(subject: String, audiences: List(String), scopes: List(String))
}

pub fn verifier(
  validate: fn(String) -> Result(Claims, Nil),
) -> authorization.Verifier(Claims) {
  use token, _correlation <- authorization.verifier("jwt")
  validate(authorization.token_value(token))
  |> result.map(fn(claims) {
    authorization.attestation(claims, claims.audiences, claims.scopes)
  })
  |> result.replace_error(authorization.BearerRejected)
}

Types

Why a request was not admitted.

pub type AdmissionError {
  MissingToken
  VerificationFailed(VerificationError)
  ResourceNotGranted
  MissingEndpointScope(Scope)
}

Constructors

  • MissingToken

    No Authorization: Bearer header, or an empty one.

  • VerificationFailed(VerificationError)
  • ResourceNotGranted

    The token was not issued for exactly this resource.

  • MissingEndpointScope(Scope)

    The token lacks a scope the endpoint requires.

What a verifier asserts about a token it accepted: the principal, the audiences the token was issued for, and its scopes. An attestation is the verifier’s claim, not cryptographic proof.

pub opaque type Attestation(principal)

A bearer token. Read it with token_value inside a verifier.

pub opaque type BearerToken

The HTTP answer to a refused request: its status and, for 401 and 403, the WWW-Authenticate header value.

pub type Challenge {
  Challenge(status: Int, www_authenticate: option.Option(String))
}

Constructors

  • Challenge(status: Int, www_authenticate: option.Option(String))

An admitted request: its verified principal, the resource and every scope the token carried.

pub opaque type Grant(principal)

The resource server’s canonical URI, the audience a token must name.

pub opaque type ProtectedResource

What an endpoint requires: the resource, the scopes every request needs, and the authorization servers its metadata names.

pub opaque type Protection

An OAuth scope.

pub opaque type Scope

Why a token, resource or scope was refused.

pub type ValueError {
  EmptyBearerToken
  InvalidResource(uri: String)
  EmptyScope
}

Constructors

  • EmptyBearerToken
  • InvalidResource(uri: String)
  • EmptyScope

Why a verifier refused a token.

pub type VerificationError {
  BearerRejected
  VerifierUnavailable
  VerifierUnmapped
  IssuedForAnotherResource
}

Constructors

  • BearerRejected

    The token is invalid, expired, revoked or not trusted: 401.

  • VerifierUnavailable

    The verifier could not decide, for example the key set or the introspection endpoint is unreachable: 503.

  • VerifierUnmapped

    The token is valid but its claims could not be mapped to a principal: 401.

  • IssuedForAnotherResource

    The token is valid but was issued for another resource, for a verifier that checks the audience itself: 401 invalid_token, with the same challenge admit gives when an attestation names another audience.

The stable classification of a VerificationError, which may gain variants: branch on it instead of matching the error exhaustively.

pub type VerificationKind {
  InvalidToken
  Unavailable
}

Constructors

  • InvalidToken

    The token is refused: 401 invalid_token.

  • Unavailable

    The verifier could not decide: 503, worth retrying later.

A token verifier with a name for telemetry.

pub opaque type Verifier(principal)

Values

pub fn admit(
  verifier: Verifier(principal),
  token: BearerToken,
  protection: Protection,
  correlation: correlation.Correlation,
) -> Result(Grant(principal), AdmissionError)

Verifies the token and admits the request when the attestation names exactly the protected resource as its only audience and carries every required scope. The verifier receives correlation, the request’s; relay/http.new_protected passes it.

pub fn attestation(
  principal: principal,
  audiences: List(String),
  scopes: List(String),
) -> Attestation(principal)

An attestation from the verifier’s validated claims. Empty audience and scope strings are ignored.

pub fn bearer_token(
  raw: String,
) -> Result(BearerToken, ValueError)

A token from its raw value.

pub fn challenge(
  protection: Protection,
  error: AdmissionError,
) -> Challenge

The RFC 6750 challenge for a refusal, carrying the RFC 9728 resource_metadata URL so a client can discover the authorization server. A missing token answers 401 without an error code, an invalid token or wrong resource 401 invalid_token, a missing scope 403 insufficient_scope with the required scopes, and an unavailable verifier 503 without a challenge.

pub fn describe_verification_error(
  error: VerificationError,
) -> String

A one-line description of the error, for logs.

pub fn grant_principal(grant: Grant(principal)) -> principal

The verified principal.

pub fn grant_resource(
  grant: Grant(principal),
) -> ProtectedResource

The resource the request was admitted for.

pub fn grant_scopes(grant: Grant(principal)) -> List(Scope)

Every scope the token carried.

pub fn has_scope(grant: Grant(principal), scope: Scope) -> Bool

Whether the token carried this scope, for per-tool checks.

pub fn metadata_path(protection: Protection) -> String

The path of the RFC 9728 metadata document for this resource: /.well-known/oauth-protected-resource followed by the resource’s path, such as /.well-known/oauth-protected-resource/mcp.

pub fn metadata_url(protection: Protection) -> String

The absolute URL of the RFC 9728 metadata document.

pub fn parse_authorization(
  header: String,
) -> Result(BearerToken, AdmissionError)

Reads the token from an Authorization header value, such as "Bearer abc". The scheme is case-insensitive.

pub fn protected(protection: Protection) -> ProtectedResource

The protected resource.

pub fn protected_resource(
  raw: String,
) -> Result(ProtectedResource, ValueError)

The protected resource from an absolute http or https URI without a fragment, such as https://mcp.example.com/mcp.

pub fn protection(
  resource: ProtectedResource,
  required_scopes: List(Scope),
) -> Protection

Protection for one resource with the scopes every request needs.

pub fn required_scopes(protection: Protection) -> List(Scope)

The scopes every request needs.

pub fn resource_metadata(protection: Protection) -> json.Json

The RFC 9728 protected-resource metadata document: the resource, its authorization servers, the scopes it requires and the header bearer method.

pub fn resource_uri(resource: ProtectedResource) -> String

The resource URI.

pub fn scope(raw: String) -> Result(Scope, ValueError)

A scope from its name.

pub fn scope_name(scope: Scope) -> String

The scope’s name.

pub fn token_value(token: BearerToken) -> String

The token’s raw value, to hand to a validator. Do not log it.

pub fn verification_kind(
  error: VerificationError,
) -> VerificationKind

The error’s classification.

pub fn verifier(
  name: String,
  verify: fn(BearerToken, correlation.Correlation) -> Result(
    Attestation(principal),
    VerificationError,
  ),
) -> Verifier(principal)

A verifier from the application’s validation function, which receives the token and the request’s correlation. name, such as "jwt" or "introspection", appears in telemetry. The correlation is the one the request’s events and its tool handler carry; pass it to a call the verifier makes, such as token introspection. It may come from the client, so it never decides admission.

pub fn verifier_name(verifier: Verifier(principal)) -> String

The verifier’s name.

pub fn with_authorization_servers(
  protection: Protection,
  issuers: List(String),
) -> Protection

Names the authorization servers, by issuer URI, that issue tokens for this resource; resource_metadata lists them.

✨ Search Document