moonbitstack/mooncred/jwt does not have a README file

    Rejected

    pub(all) suberror Rejected {
    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)
    } 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.

    Rejected::equal

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

    Rejected::not_equal

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

    Rejected::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 Rejected

    Read an algorithm by name.

    Alg::to_repr

    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

    OnClash

    pub(all) enum OnClash[T] {
    Ignore
    Panic
    Handle((T, T) -> T)
    }

    What to do when a setting is given twice.

    Handle receives the base and the merged result and returns whichever it wants, so a caller can audit, refuse, or merge some other way entirely.

    There is no Warn: a library has nowhere to warn to. It arrives when moonlog does.

    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

    Wins

    pub(all) enum Wins {
    Base
    Extra
    } derive(Eq,
    Debug
    )

    Which side wins when a setting is given twice.

    Every function here that takes both a base and something layered over it says which one wins, and lets a caller say otherwise. The defaults differ by function and each one says why: verify lets the override win, because a per-call argument is more specific than a policy assembled once; sign lets the base win, because the header it computes describes the signature it is about to make.

    Wins::equal

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

    Wins::not_equal

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

    Wins::to_repr

    claims_unverified

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

    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 Rejected

    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.

    policy

    let policy : Policy

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

    sign

    fn sign(claims : Map[String, Json], alg : Alg, key : &
    Signer
    , typ? : String?, extra? : Map[String, Json], wins? : Wins, clash? : OnClash[Map[String, Json]]) -> String

    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.

    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 Rejected

    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.

    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.

    Source Files