moonbitstack/mooncred/jwt does not have a README file

    Refused

    pub(all) suberror Refused {
    Shape(String)
    Algorithm(want~ : String, got~ : String)
    Signature
    Expired(at~ : Double)
    TooEarly(at~ : Double)
    Issuer(want~ : String, got~ : String)
    Audience(want~ : Array[String])
    Subject(want~ : String, got~ : String)
    IssuedAt(at~ : Double)
    Type(want~ : String, got~ : String)
    Missing(claim~ : String)
    Encoding(want~ : Bool, got~ : Bool)
    Critical(name~ : String)
    } derive(Eq,
    Debug
    )

    Why a token was not accepted.

    Each reason is separate so a caller can map it to the right answer — an expired token deserves a different response from a forged one — and so a log says which check failed rather than that one did.

    Refused::equal

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

    Refused::not_equal

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

    Refused::to_repr

    Alg

    pub(all) enum Alg {
    HS256
    HS384
    HS512
    RS256
    RS384
    RS512
    PS256
    PS384
    PS512
    ES256
    ES384
    ES512
    EdDSA
    } derive(Eq,
    Debug
    )

    A JOSE signature algorithm (RFC 7518 §3.1), and the one RFC 8037 adds.

    none is not here and never will be. It is the algorithm that made "verified" mean "not verified" in a dozen libraries, and a type that cannot express it cannot be talked into accepting it.

    Alg::equal

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

    Alg::name

    fn Alg::name(self : Alg) -> String

    The name this algorithm goes by in a header.

    Alg::not_equal

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

    Alg::of

    fn Alg::of(name : StringView) -> Alg raise Refused

    Read an algorithm by name.

    Alg::to_repr

    Check

    pub(all) struct Check {
    alg : Alg
    key : &
    Verifier

    }

    What a caller is willing to accept: one algorithm and the key that checks it.

    A message may carry several signatures by different keys under different algorithms; each check is tried against the signatures that name its algorithm, and one that verifies ends the search. Nothing is tried that the caller did not offer, so a signature added under an algorithm this program happens to link is not a way in.

    Clock

    pub(all) enum Clock {
    At(Int64)
    Ignored
    } derive(Eq,
    Debug
    )

    When the time claims are measured against.

    A library has no clock — reading one is a side effect and there is no reason for a token verifier to have that power — so the instant comes in from the caller. Ignored is the way to read a token without asking whether it has expired: a debugging tool, an audit of tokens already collected, a migration that has to read what was issued last year. It is spelled out at the call site because the alternative is a verifier that quietly stopped checking.

    Clock::equal

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

    Clock::not_equal

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

    Clock::to_repr

    Keyed

    type Keyed

    A keyed MAC, standing in as both signer and verifier.

    HMAC is symmetric: the same key that signs verifies. What it is not is a signature — anyone who can check an HS256 token can mint one — so a token verified this way says only that someone holding the shared secret issued it.
    impl Signer for Keyed
    impl Verifier for Keyed

    Keyed::sign

    fn Keyed::sign(self : Keyed, msg : BytesView) -> Bytes

    Keyed::verify

    fn Keyed::verify(self : Keyed, msg : BytesView, tag : BytesView) -> Bool

    Message

    pub struct Message {
    payload : Bytes?
    signatures : Array[Signature]
    encoded : Bool
    }

    A payload and the signatures over it (RFC 7515 §7.2).

    The payload is octets: a claims set, a document, a file. It is None only for a message whose payload travels beside it — RFC 7515 §A.5 — in which case a verifier is given the octets to check against.

    Every signature in one message sees the payload the same way. A message whose first signature says the payload is base64url and whose second says it is not describes two different payloads in one member, so Message::sign refuses the second.

    Message::detached

    fn Message::detached(self : Message) -> Message

    The same message with its payload left out, for a caller that sends the octets by another road and wants the signature to cover them anyway.

    Message::encoded

    fn Message::encoded(self : Message) -> Bool

    Whether the payload travels base64url encoded. false is RFC 7797's b64: false.

    Message::flattened

    The message as the flattened serialisation: one signature, its members beside the payload.

    This is a convenience for the one-signature case, and a refusal for any other, because the flattened form has nowhere to put a second signature.

    Message::json

    The message as the general JSON serialisation: a payload and an array of signatures.

    Message::new

    fn Message::new(payload : BytesView) -> Message

    Start a message over a payload, with no signatures yet.

    Message::payload

    fn Message::payload(self : Message) -> Bytes?

    The payload, or None for a message that carries it beside the text.

    Message::sign

    fn Message::sign(self : Message, alg : Alg, key : &
    Signer
    , signed? : Map[String,
    Json
    ], header? : Map[String,
    Json
    ], b64? : Bool, bare? : Bool) -> Message raise Refused

    Add a signature by this key under this algorithm, and return the message.

    signed is the header that is signed and header the one that is not; alg goes into the signed one. bare is RFC 7515 §A.5's "protecting content only": no signed header at all, with alg and everything else in the unprotected one. That form authenticates the payload and nothing about how to read it, so the reader of one must still be told which algorithm and key to try — which is the only reason it is safe enough to offer.

    The two headers of one signature may not name the same parameter twice.

    Message::signatures

    fn Message::signatures(self : Message) -> Array[Signature]

    The signatures, in the order they were added.

    Policy

    pub(all) struct Policy {
    leeway : Int64
    expiry : Bool
    not_before : Bool
    issued_at : Bool
    issuer : String?
    audience : Array[String]
    subject : String?
    typ : String?
    required : Array[String]
    } derive(Eq,
    Debug
    )

    What a token has to satisfy beyond its signature.

    Every field is a check that is either off or told what to expect, and every default is what the mainstream libraries do: exp and nbf are checked, iat is not, and iss, aud, sub and the header's typ are looked at only when the caller says what to expect. Nothing is enforced implicitly and nothing is skipped implicitly.

    One of these is assembled once for a deployment and passed to every verification, which is what jsonwebtoken's Validation, .NET's TokenValidationParameters and jose's options object are for.

    Policy::equal

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

    Policy::new

    fn Policy::new(leeway? : Int64, expiry? : Bool, not_before? : Bool, issued_at? : Bool, issuer? : String, audience? : Array[String], subject? : String, typ? : String, required? : Array[String]) -> Policy

    Build a policy by naming the parts that differ from the default.

    The same thing can be written { ..@jwt.policy, expiry: false }; this form exists because a named argument reads better when several parts differ. issuer, subject and typ can be set here but not cleared: the preset leaves all three unset, so there is nothing to clear. Starting from a base of one's own, { ..mine, issuer: None } is how a check is dropped.

    Policy::not_equal

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

    Policy::to_repr

    Signature

    pub(all) struct Signature {
    signed : String?
    header : Map[String,
    Json
    ]?
    signature : String
    }

    One signature over a payload, as the JSON serialisations of RFC 7515 §7.2 carry it.

    protected is the base64url text that was signed, kept as text rather than decoded: a verifier must hash exactly the octets that arrived, and re-encoding a decoded header is how a message that verified somewhere else fails here.

    Verified

    pub(all) struct Verified {
    payload : Bytes
    index : Int
    header : Map[String,
    Json
    ]
    }

    A payload that one of the checks accepted.

    claims_unverified

    fn claims_unverified(token : StringView) -> Map[String,
    Json
    ] raise Refused

    The claims of a token that has not been verified.

    Whoever sent the token wrote these. Reading them before verify has returned is reading an attacker's input; it is here because a gateway sometimes must route on iss before it knows which key to fetch, and for no other reason.
    fn header(token : StringView) -> Map[String,
    Json
    ] raise Refused

    The header of a token that has not been verified.

    This is for one thing: reading kid to choose which key to verify with. Nothing else in here has been checked by anybody.

    mac

    Take a way of making a keyed MAC and use it as a JWT key.

    It takes a factory rather than a MAC because one state cannot authenticate two messages, and a key outlives the token it was used on.

    message

    fn message(text : StringView) -> Message raise Refused

    A JSON serialisation read as a message, without checking anything.

    The signatures are read and the payload is decoded; nothing is verified. This is what a caller uses to look at kid and decide which keys to offer, and for nothing else.

    policy

    let policy : Policy

    The policy a verification uses when the caller does not say otherwise.

    sign

    Sign a claims set as a compact JWT.

    extra goes into the header beside alg and typ — kid is what it is usually for, so a verifier can pick the right key out of a set before it verifies anything.

    typ is "JWT", which is what every library writes and what RFC 7519 §5.1 recommends. None leaves it out; another string puts that in — RFC 9068's access tokens want "at+jwt".

    extra cannot quietly rewrite alg or typ. The header describes the signature this function is about to make; a token whose header says none while the bytes say HS256 is not a configuration choice, it is a mistake, so wins starts at Base and clash at Panic. A caller who means it can say wins=Extra, clash=Ignore.

    The key is whatever can sign; this package never chooses an algorithm's implementation, so a program that issues only HS256 tokens links only HMAC.

    sign_bytes

    fn sign_bytes(payload : BytesView, alg : Alg, key : &
    Signer
    , b64? : Bool, typ? : String, extra? : Map[String,
    Json
    ], detached? : Bool) -> String raise Refused

    Sign octets into the compact serialisation.

    b64 is RFC 7797: false signs the payload itself instead of a base64url copy of it, so a token can carry octets that must arrive as they were signed. With false the payload must be ASCII and must not contain a dot, because the compact form is cut at its dots.

    detached signs the payload without carrying it (RFC 7515 §A.5): the middle segment is empty and the octets travel by another road, to be handed to verify_bytes by the caller.

    verify

    fn verify(token : StringView, alg : Alg, key : &
    Verifier
    , now~ : Clock, policy? : Policy, wins? :
    Wins
    , clash? :
    OnClash
    [Policy], leeway? : Int64, expiry? : Bool, issuer? : String, audience? : Array[String]) -> Map[String,
    Json
    ] raise Refused

    Verify a compact JWT and return its claims.

    The algorithm is the caller's, not the token's. A verifier that reads alg out of the header and then trusts it is how an RS256 public key gets used as an HS256 shared secret, and how none gets accepted; here the header must match what the caller already decided, or the token is refused.

    The signature is checked before any claim is read, so a forged token is never inspected for its contents.

    Configuration has three layers, each overriding the one before: @jwt.policy < the policy given here < the named arguments given here. wins turns the last two around; clash decides whether a setting given twice with different values is silent, reported, fatal, or yours to arbitrate. Both are moonknob's, shared by every library in the family.

    now is not configuration but data: the instant the time claims are measured against, or Ignored to read a token without asking whether it has expired — a debugging tool, an audit of tokens already collected, a migration that has to read what was issued last year.

    verify_bytes

    fn verify_bytes(token : StringView, alg : Alg, key : &
    Verifier
    , b64? : Bool, payload? : Bytes) -> Bytes raise Refused

    Verify a compact JWS and return its payload octets.

    b64 says how the caller expects the payload to travel, and a token that says otherwise is refused: RFC 7797 §6 makes the choice part of what is signed precisely so that two readers cannot disagree about which octets the signature covered.

    payload is for a token that carries no payload of its own: the octets arrive from wherever the sender put them, and the signature is checked against those.

    verify_json

    fn verify_json(text : StringView, checks~ : Array[Check], payload? : Bytes) -> Verified raise Refused

    Verify a JSON serialisation — general or flattened, it is told apart by its shape — and return the payload that one of the checks accepted.

    payload is for a message whose payload travels beside the text: RFC 7515 §A.5 leaves the member out, and the octets come from the caller that sent it. A message that carries its own payload and a caller that supplies one are a disagreement, and are refused.

    Source Files