moonbitstack/moonmcp/auth does not have a README file

    Refused

    pub(all) suberror Refused {
    Refused(message~ : String)
    }

    Why an OAuth client could not go on, in words a person reads.

    Bearer

    pub struct Bearer {
    // private fields
    }

    How a transport checks a bearer token (← the authorization section of the protocol): a request without one is answered 401 with a WWW-Authenticate challenge, and one whose token does not verify is answered the same way with invalid_token.

    Two ways to build one: Bearer::hmac for a token signed with a shared secret, which is what a test and a single-tenant deployment use, and Bearer::own for anything else — an RSA or ECDSA key, a JSON Web Key Set fetched from the issuer, an opaque token looked up in a table. own takes the whole decision, so this library never has an opinion about how a token is judged, only about what happens when it is not.

    Bearer::check

    fn Bearer::check(self : Bearer, authorization : String) -> Result[Principal, String]

    Check the Authorization header of a request. Err carries the reason the token is refused, phrased for the client.

    Bearer::hmac

    fn Bearer::hmac(secret~ : String, alg? :
    Alg
    , issuer? : String, audience? : Array[String], leeway? : Int64, clock? : () -> Int64) -> Bearer

    Check tokens signed with secret by a shared key: alg is the algorithm the verifier uses, and a token that names another one is refused rather than trusted.

    issuer and audience are checked when given, so a token minted for another service does not open this one. clock is the instant in seconds since the epoch that expiry is measured against, which a test fixes and a deployment leaves alone.

    Bearer::own

    fn Bearer::own(check~ : (String) -> Result[Principal, String]) -> Bearer

    Check tokens with check, which returns the caller or the reason the token is refused. The reason travels to the client in error_description, so it should say what is wrong without saying anything a stranger could use.

    Challenge

    pub(all) struct Challenge {
    resource_metadata : String?
    scope : String?
    error : String?
    error_description : String?
    } derive(Eq,
    Debug
    )

    The Bearer challenge of a 401 or 403: where the resource metadata is, which scope would satisfy it, and what went wrong.

    Challenge::equal

    fn Challenge::equal(Challenge, Challenge) -> Bool

    Challenge::not_equal

    fn Challenge::not_equal(x : Challenge, y : Challenge) -> Bool

    Challenge::parse

    fn Challenge::parse(header : String) -> Challenge?

    Read the Bearer challenge out of a WWW-Authenticate header. Parameters are name=value with the value optionally quoted; a name repeated keeps the first, which is what a client needs.

    Credentials

    pub(all) enum Credentials {
    Basic
    Post
    Public
    } derive(Eq,
    Debug
    )

    How the token endpoint is asked to authenticate the client.

    Credentials::authorization

    fn Credentials::authorization(self : Credentials, registration : Registration) -> String?

    The Authorization header value for this method, when it uses one. Basic credentials are client_id:client_secret in base64, both percent-encoded as RFC 6749 §2.3.1 requires.

    Credentials::equal

    fn Credentials::equal(Credentials, Credentials) -> Bool

    Credentials::form

    fn Credentials::form(self : Credentials, registration : Registration) -> Array[(String, String)]

    The fields this method puts in the token request's body.

    Credentials::not_equal

    fn Credentials::not_equal(x : Credentials, y : Credentials) -> Bool

    Credentials::select

    fn Credentials::select(registration : Registration, supported : Array[String]) -> Credentials

    The method to use, given what the server says it supports and how the client was registered. Priority is the one a client that holds a secret should follow: what registration returned when the server lists it, else HTTP Basic, else the secret in the body, else nothing.

    Grant

    pub(all) enum Grant {
    AuthorizationCode
    ClientCredentials
    } derive(Eq,
    Debug
    )

    Which grant a client is set up to use: the interactive one, or the one a client makes on its own behalf.

    Grant::equal

    fn Grant::equal(Grant, Grant) -> Bool

    Grant::not_equal

    fn Grant::not_equal(x : Grant, y : Grant) -> Bool

    Grant::to_repr

    Metadata

    pub(all) struct Metadata {
    client_name : String
    redirect_uris : Array[String]
    grant_types : Array[String]
    response_types : Array[String]
    token_endpoint_auth_method : String
    scope : String?
    } derive(Eq,
    Debug
    )

    What a client says about itself: at registration, and in the document a URL client identifier points at.

    Metadata::body

    The body of a registration request.

    Metadata::equal

    fn Metadata::equal(Metadata, Metadata) -> Bool

    Metadata::new

    fn Metadata::new(client_name~ : String, redirect_uris~ : Array[String], scope? : String, grant_types? : Array[String], response_types? : Array[String], token_endpoint_auth_method? : String) -> Metadata

    A client's metadata. refresh_token is among the grants because a client that can keep a session wants one; none is the authentication method of a public client that a loopback redirect makes safe with PKCE.

    Metadata::not_equal

    fn Metadata::not_equal(x : Metadata, y : Metadata) -> Bool

    Metadata::to_repr

    Pkce

    pub(all) struct Pkce {
    verifier : String
    challenge : String
    } derive(Eq,
    Debug
    )

    A PKCE pair: verifier goes in the token request, challenge in the authorization request. Only S256 is used — the plain method protects nothing, and the protocol requires S256 of every client that can do it.

    Pkce::equal

    fn Pkce::equal(Pkce, Pkce) -> Bool

    Pkce::new

    fn Pkce::new() -> Pkce raise Refused

    A fresh pair, from 32 bytes of entropy: 43 characters, which is the shortest length RFC 7636 allows and enough to be unguessable.

    Pkce::not_equal

    fn Pkce::not_equal(x : Pkce, y : Pkce) -> Bool

    Pkce::of

    fn Pkce::of(verifier : String) -> Pkce

    The pair for a verifier already chosen: the challenge is its SHA-256 digest in base64url, without padding.

    Pkce::to_repr

    Principal

    pub(all) struct Principal {
    subject : String?
    claims : Map[String,
    Json
    ]
    } derive(Eq,
    Debug
    )

    The caller a token vouched for: its sub, and every claim as it arrived.

    A claim is whatever the issuer put there — a scope list, a tenant, a role. Nothing here interprets them; a handler reads what it needs.

    Principal::equal

    fn Principal::equal(Principal, Principal) -> Bool

    Principal::has_scope

    fn Principal::has_scope(self : Principal, scope : String) -> Bool

    Whether the token grants scope, read from the scope claim (space separated) or scp (a list), which are the two spellings in use.

    Principal::not_equal

    fn Principal::not_equal(x : Principal, y : Principal) -> Bool

    Redirect

    pub(all) struct Redirect {
    code : String
    state : String?
    } derive(Eq,
    Debug
    )

    The code and state a redirect carried, or why there is none.

    Redirect::equal

    fn Redirect::equal(Redirect, Redirect) -> Bool

    Redirect::not_equal

    fn Redirect::not_equal(x : Redirect, y : Redirect) -> Bool

    Redirect::parse

    fn Redirect::parse(url : String) -> Redirect raise Refused

    Read a redirect back: the code, the state to compare with the one sent, and an error if the server refused instead.

    Redirect::to_repr

    Registration

    pub(all) struct Registration {
    client_id : String
    client_secret : String?
    token_endpoint_auth_method : String?
    } derive(Eq,
    Debug
    )

    What a client is known by: the identifier the server issued, or the one a deployment was given, and the secret if it has one.

    Registration::equal

    Registration::not_equal

    fn Registration::not_equal(x : Registration, y : Registration) -> Bool

    Registration::parse

    Read a registration response.

    Resource

    pub(all) struct Resource {
    resource : String
    authorization_servers : Array[String]
    scopes_supported : Array[String]?
    } derive(Eq,
    Debug
    )

    What a protected resource says about itself (RFC 9728): the identifier tokens for it must be minted for, and the authorization servers that mint them.

    Resource::equal

    fn Resource::equal(Resource, Resource) -> Bool

    Resource::not_equal

    fn Resource::not_equal(x : Resource, y : Resource) -> Bool

    Resource::parse

    Read protected resource metadata.

    Resource::probes

    fn Resource::probes(resource_url : String, hinted? : String) -> Array[String]

    The documents to try for protected resource metadata, in the order the specification asks for: the one a challenge pointed at, else the path-aware location, else the root. A server that publishes the path-aware location is not asked for the root one — a client that asks both is told apart by that.

    Resource::to_repr

    Server

    pub(all) struct Server {
    issuer : String
    authorization_endpoint : String?
    token_endpoint : String
    registration_endpoint : String?
    scopes_supported : Array[String]?
    response_types_supported : Array[String]
    grant_types_supported : Array[String]
    token_endpoint_auth_methods_supported : Array[String]
    token_endpoint_auth_signing_alg_values_supported : Array[String]
    code_challenge_methods_supported : Array[String]?
    client_id_metadata_document_supported : Bool
    } derive(Eq,
    Debug
    )

    What an authorization server says it can do (RFC 8414 and OpenID Connect Discovery). A field the document leaves out keeps the default the metadata specification gives it, except where the default is "not offered".

    Server::at_root

    fn Server::at_root(base : String) -> Server

    The endpoints a 2025-03-26 server has when it publishes no metadata at all: the three OAuth paths at the server's own root (MCP's authorization section before it named RFC 8414).

    Server::equal

    fn Server::equal(Server, Server) -> Bool

    Server::not_equal

    fn Server::not_equal(x : Server, y : Server) -> Bool

    Server::offers

    fn Server::offers(self : Server, grant : String) -> Bool

    Whether this server offers the grant.

    Server::parse

    Read authorization server metadata. issuer and token_endpoint are the two a client cannot work without.

    Server::probes

    fn Server::probes(authorization_server : String) -> Array[String]

    The documents to try for authorization server metadata, in the order RFC 8414 and OpenID Connect Discovery put them: the path inserted after the well-known prefix, then the two OpenID spellings.

    Server::serves_authorization_code

    fn Server::serves_authorization_code(self : Server) -> Bool

    Whether this server can run an authorization-code flow with PKCE.

    Server::to_repr

    Tokens

    pub(all) struct Tokens {
    access_token : String
    token_type : String
    expires_in : Int64?
    refresh_token : String?
    scope : String?
    } derive(Eq,
    Debug
    )

    What the token endpoint answered.

    Tokens::equal

    fn Tokens::equal(Tokens, Tokens) -> Bool

    Tokens::good_until

    fn Tokens::good_until(self : Tokens, issued : Int64) -> Int64?

    When the token stops being good, in seconds since the epoch, measured from issued; None when it names no lifetime, which a client reads as "until the server says otherwise".

    Tokens::not_equal

    fn Tokens::not_equal(x : Tokens, y : Tokens) -> Bool

    Tokens::parse

    Read a token response. access_token is the only field a client cannot do without; a server that omits token_type is taken at its word that the token goes in the Authorization header.

    Tokens::renewed

    fn Tokens::renewed(self : Tokens, fresh : Tokens) -> Tokens

    The token a refresh left this client with. A server may rotate the refresh token, so a response that carries one replaces the old; one that carries none leaves the old in place rather than throwing the only key away. The same holds for the scope: what a response does not name is what was asked for, which is what this client already had.

    Tokens::to_repr

    authorization_code_form

    fn authorization_code_form(code~ : String, redirect_uri~ : String, verifier~ : String, resource? : String, scope? : String) -> Array[(String, String)]

    The body of an authorization-code exchange (RFC 6749 §4.1.3) with the PKCE verifier and the RFC 8707 resource indicator.

    authorize_url

    fn authorize_url(server : Server, registration : Registration, redirect_uri : String, state : String, pkce : Pkce, scope? : String, resource? : String) -> String raise Refused

    The URL to send the user to: response_type=code, the client, the redirect, the state, the S256 challenge, and the resource indicator RFC 8707 asks for.

    client_credentials_form

    fn client_credentials_form(resource? : String, scope? : String) -> Array[(String, String)]

    The body of a client-credentials request (RFC 6749 §4.4).

    form_encode

    fn form_encode(pairs : Array[(String, String)]) -> String

    pairs as an application/x-www-form-urlencoded body.

    new_state

    fn new_state() -> String raise Refused

    The state parameter of an authorization request: a value the client makes up and checks on the way back, so a redirect it did not cause is not taken.

    origin_of

    fn origin_of(url : String) -> String

    Everything in url before its path: scheme, authority, and nothing else.

    path_of

    fn path_of(url : String) -> String

    The path of url, from the slash after the authority up to the query or fragment. / when it has none, which is what the well-known lookups expect.

    percent_encode

    fn percent_encode(text : StringView) -> String

    Percent-encode everything but the unreserved characters, which is what a form body and an OAuth parameter take.

    query_of

    fn query_of(url : String) -> String

    The query of url, without the ?.

    query_value

    fn query_value(query : String, name : String) -> String?

    The value of name in a query string, with + read as a space as application/x-www-form-urlencoded says and the escapes decoded.

    refresh_form

    fn refresh_form(refresh_token~ : String, resource? : String, scope? : String) -> Array[(String, String)]

    The body of a refresh (RFC 6749 §6).

    same_origin

    fn same_origin(url : String, expected : String) -> Bool

    Whether url names the same resource as expected: the scheme and host compared without regard to case, the rest exactly. RFC 8707 lets a client refuse a protected resource that names another origin.

    scope_for

    fn scope_for(challenge : Challenge?, resource : Resource?, configured : String?) -> String?

    The scope for an authorization request. None when nothing names one, in which case the request carries no scope parameter at all: sending an empty one is not the same thing to every server.

    scope_within

    fn scope_within(asked : String?, granted : String?) -> Bool

    Whether every scope asked names is one granted already carries. A challenge that names no scope asks nothing new; one that names a scope this client was never granted is a step up, and a refresh cannot answer it (RFC 6749 §6 keeps a refresh inside the scope it was given).

    target_of

    fn target_of(url : String) -> String

    The whole of url as a request line carries it: path and query.

    trimmed_path_of

    fn trimmed_path_of(url : String) -> String

    The path of url without its trailing slash, which is the form the well-known prefixes are built from.

    widen_scope

    fn widen_scope(current : String?, asked : String?) -> String?

    The union of two scope strings, the current one first, without repeats.

    with_offline_access

    fn with_offline_access(scope : String?, server : Server, grant : Grant) -> String?

    The scope to ask for when this client wants a refresh token. The authorization server's own offline_access is what makes it hand one out, and a server that does not list it must not be asked (SEP-2207). The client-credentials grant gets no refresh token at all (RFC 6749 §4.4.3), so nothing is added for it.

    with_query

    fn with_query(url : String, query : Array[(String, String)]) -> String

    url with query as its query, replacing one it had.