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
-
MissingTokenNo
Authorization: Bearerheader, or an empty one. -
VerificationFailed(VerificationError) -
ResourceNotGrantedThe 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
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
-
BearerRejectedThe token is invalid, expired, revoked or not trusted: 401.
-
VerifierUnavailableThe verifier could not decide, for example the key set or the introspection endpoint is unreachable: 503.
-
VerifierUnmappedThe token is valid but its claims could not be mapped to a principal: 401.
-
IssuedForAnotherResourceThe token is valid but was issued for another resource, for a verifier that checks the audience itself: 401
invalid_token, with the same challengeadmitgives 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
-
InvalidTokenThe token is refused: 401
invalid_token. -
UnavailableThe verifier could not decide: 503, worth retrying later.
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 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_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 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.