pub enum AuthError {
Show 51 variants
AlgNone,
AlgNotWhitelisted,
AlgHmacRejected,
AlgRsaRejected,
AlgEcdsaRejected,
HeaderJku,
HeaderX5u,
HeaderJwk,
HeaderX5c,
HeaderCrit,
KidUnknown,
TypMismatch,
NestedJws,
JwePayload,
HeaderExtraParam,
HeaderB64False,
ExpMissing,
Expired,
ExpUpperBound,
AudMissing,
AudMismatch,
IssMismatch,
IatMissing,
IatFuture,
NotYetValid,
JtiMissing,
SubMissing,
ClientIdMissing,
TokenTypeMismatch,
InvalidNumericType,
JwsJsonRejected,
DuplicateJsonKeys,
LaxBase64,
OversizedToken,
SubFormatInvalid,
AccountTypeInvalid,
CapsShapeInvalid,
ScopesShapeInvalid,
ScopesTooLong,
DlgDepthInvalid,
AdminBandRejected,
UnknownClaim(String),
JtiReplayed,
ReplayCacheUnavailable,
SessionRevoked,
SessionLookupUnavailable,
SessionVersionStale,
SessionVersionLookupUnavailable,
NotJwsCompact,
HeaderUnparseable,
PayloadUnparseable,
}Variants§
AlgNone
M01: token header carries alg: none (or a value the library cannot
parse to a known Algorithm).
AlgNotWhitelisted
M02: header alg is parseable but not in the per-request whitelist.
AlgHmacRejected
M03: HMAC family (HS256/HS384/HS512) rejected — confusion attack
against asymmetric public keys.
AlgRsaRejected
M04: RSA family (RS*/PS*) rejected.
AlgEcdsaRejected
M05: ECDSA family (ES*) rejected.
HeaderJku
M07: header carries jku (URL-loaded JWK Set).
HeaderX5u
M08: header carries x5u (URL-loaded X.509 chain).
HeaderJwk
M09: header carries inline jwk.
HeaderX5c
M10: header carries x5c (inline X.509 chain).
HeaderCrit
M11: header carries crit with unknown extensions.
KidUnknown
M12: kid missing or unknown to the server-pinned KeySet.
TypMismatch
M13/M13a: typ does not equal the configured value (default
at+jwt). Strict equality — JWT is also rejected.
NestedJws
M14: nested JWS (payload is itself a JWT) — defended via cty header
inspection.
JwePayload
M15: token is JWE (5-part) — encryption forbidden in this profile.
HeaderExtraParam
M16: header contains parameters outside the whitelist
(typ, alg, kid).
HeaderB64False
M16a: header carries b64=false (RFC 7797 unencoded payload).
ExpMissing
M17: exp claim absent. RFC 8725 §3.10 — a token without an expiry
contract has no admissibility window; reject before any value check.
Expired
M18: exp is in the past. RFC 8725 §3.10 — leeway = 0; the engine
refuses any token whose expiry timestamp precedes the current
instant. Distinct from ExpMissing: M17 fires when the claim is
absent, M18 fires when it’s present but stale.
ExpUpperBound
M19: exp exceeds the per-profile upper bound (24h for access,
200d for refresh — Phase 2 is access-only; refresh issuance lands
Phase 4). Bounds the blast radius of a leaked token: a malicious
issuer cannot mint near-immortal credentials.
AudMissing
M20: aud claim absent. Without an audience binding the engine
cannot enforce the verifier-specific match (M21/M22) — refuse
before any value check.
AudMismatch
M21 + M22: aud value does not match cfg.audience. Phase 1’s
M06 documented-collapse pattern applies — M21 covers the string
form, M22 covers the array form, both surface the same audit
signal. The variant carries the M-ID via the #[error] string;
audit logs disambiguate via the cfg+token state.
IssMismatch
M23: iss is missing OR does not match the pinned issuer
(cfg.issuer). Both cases collapse into one variant — the audit
signal is identical: this token did not come from the trusted
issuer. Distinguishing missing-vs-wrong adds no useful diagnostic
(an attacker who omits iss and one who forges a wrong iss are
both probing for issuer trust confusion).
IatMissing
M24 (first clause): iat claim absent. Without an issuance
timestamp the engine can neither bound the token’s age (M19) nor
enforce M24’s “must be in past” rule. M24’s future-iat clause
surfaces as IatFuture.
IatFuture
M24 (second clause) + M25: iat is in the future beyond the 60s
clock-skew leeway. Phase 1’s M06 documented-collapse pattern
applies — M24’s must-be-in-past predicate and M25’s far-future
ceiling enforce the same condition (iat > now + 60s); they
share the variant. Audit logs disambiguate via the iat value
itself (60s vs hours-in-the-future).
NotYetValid
M26: nbf (not-before) is present and in the future — the token
has not yet entered its admissibility window. Distinct from
IatFuture: nbf is the issuer’s explicit “valid from” boundary,
where iat is the issuance instant. nbf is optional; absence is
not an error.
JtiMissing
M27: jti claim absent. RFC 7519 §4.1.7 — the unique token
identifier is the replay-cache key (M35). Without it the engine
cannot enforce one-shot semantics on per-token operations.
SubMissing
M28: sub claim absent. RFC 7519 §4.1.2 — the subject identifies
the principal the token is about; no useful authorization
decision can follow when it’s missing.
ClientIdMissing
M28a: client_id claim absent. RFC 9068 §2.2 mandates this for
access JWTs so the resource server can identify the originating
OAuth client (audit, per-client rate limits).
TokenTypeMismatch
M29: cat (token category) is missing or not the expected value.
RFC 9068 §2.2 + ppoppo extension — cat ∈ {access, refresh} is a
payload-level discriminator that lets a single verifier refuse
type-confusion attempts (refresh token at an access endpoint).
Phase 2 verifies access tokens only; Phase 4 (refresh issuance)
generalizes via cfg.expected_cat. Missing-cat collapses into
the same variant — audit signal is identical (untrusted token
type).
InvalidNumericType
M30: a numeric claim (exp/iat/nbf) is present but not a
JSON integer. RFC 8725 §2.4 — string-coerced numerics are a
classic substitution vector ("exp": "9999" parsed as a “valid”
future expiry). Engine refuses any non-integer numeric claim
before the value-violation rules can fire.
JwsJsonRejected
M31: input is JWS JSON serialization (or any non-compact form). RFC 8725 §2.4 — the profile accepts JWS Compact only. JSON-form JWS expands the implementation surface and has historically carried polyglot-payload attacks; refuse before any segment parser runs.
DuplicateJsonKeys
M32: header or payload JSON contains duplicate top-level keys. RFC 7515 §3 mandates rejection, but serde_json silently keeps the last occurrence by default — making the smuggling case (a forger duplicates a claim hoping the verifier reads one value while a downstream consumer reads another) invisible. Engine pre-validates every JSON object via a key-uniqueness Visitor before parsing.
LaxBase64
M33: a segment contains characters from the standard base64
alphabet (+, /, =) — RFC 8725 §2.4 requires strict
base64url (URL_SAFE_NO_PAD: only A-Z a-z 0-9 - _). Standard
b64 chars are rejected with their own variant so audit logs
distinguish “intentional + injection” from generic decode
failures (which surface as HeaderUnparseable /
PayloadUnparseable).
OversizedToken
M34: total token length exceeds cfg.max_token_size (8 KB
default for the access-token profile). A large token is either
a misconfigured issuer (extras bloating beyond a reasonable
claim set) or a denial-of-service vector (parser amplification).
Engine refuses oversized input before any segment parsing runs.
SubFormatInvalid
M39: sub is present but not a 26-character Crockford-base32 ULID.
PAS-issued tokens carry ppnum_id (Human ULID) or an AI-agent ULID
in sub; any other shape is either an issuer drift or a forgery
attempt. Distinct variant so audit logs distinguish “sub missing”
(M28) from “sub ill-formed” (M39).
AccountTypeInvalid
M40: account_type is present but not in the whitelist
{"human", "ai_agent"}. The claim is optional (legacy tokens
minted before the field existed are admitted), but a present-
but-unknown value is a forgery signal — the issuer never emits
arbitrary strings here. Renamed from the matrix’s earlier
actor to avoid collision with RFC 8693 token-exchange.
CapsShapeInvalid
M41: caps is present but the wire shape is wrong (not a JSON
array of strings). The default-deny invariant lives in the
interpretation: absent/empty both mean “no capabilities”, and
any non-empty value MUST be an array of strings. A string-typed
caps: "admin" is the canonical confusion — a forger hoping the
verifier reads it as a one-element list.
ScopesShapeInvalid
M42 (shape): scopes is present but not a JSON array of strings.
Mirrors CapsShapeInvalid — collapsed into its own variant
because the audit signal is meaningfully different (a scope
confusion attack reads a different threat model than a
capability shape attack).
ScopesTooLong
M42 (length): scopes has more than 256 entries. Bounds the
per-token audit surface and stops a misconfigured issuer (or a
forger who got hold of a signing key) from minting a token whose
authorization vector is itself a DoS — a 10k-entry scopes array
pessimizes every per-request scope check.
DlgDepthInvalid
M43: dlg_depth is present but exceeds the 4-step delegation
chain bound (or is the wrong wire shape — non-integer, negative).
Bounds the audit-trail explosion of arbitrarily deep Token
Exchange chains; dlg_depth = 4 is the inclusive bound, matching
RFC §6.5. Single variant covers both shape and bound errors —
audit signal is “delegation chain rejected”, and the value
itself surfaces in structured logging at the rejection site.
AdminBandRejected
M44 (band gate): token claims admin: true but the supporting
active_ppnum is either absent or its first 3 digits don’t fall
in the admin allocation band. PAS issues admin tokens only on
ppnums minted from the admin band, so a token outside the band is
either a forgery (with a stolen signing key) or an issuer drift.
This is defense-in-depth on top of is_admin DB lookup
(STANDARDS_AUTH_PPOPPO §3.2 — DB is the source of truth); it
turns a stolen-key forgery into an “active_ppnum needs to be
banded” forgery, narrowing the attack window meaningfully.
UnknownClaim(String)
M45: payload contains a claim outside the engine’s strict
allowlist. The PAS issuance pipeline only emits claims listed
in engine::check_domain::ALLOWED_CLAIMS; anything else is a
forgery / misconfiguration / PII smuggling attempt (M45 is the
“no PII in payload” defense — email / phone / name get
rejected by name). The variant carries the offending claim
name for audit logs so operators can see at a glance which
claim triggered the rejection.
JtiReplayed
M35: jti has been seen within the replay-cache TTL — replayed
token. Engine refuses on the second sighting; implementations of
ReplayDefense MUST treat the cache check and record as a single
atomic primitive (KVRocks SET NX EX, equivalent) to avoid a
TOCTOU window between check and record.
M35 (substrate transient): replay-cache substrate is unreachable.
Engine fails closed — admitting on substrate failure would let a
replayer slip through during the outage. Audit logs surface this
as a SEPARATE signal from JtiReplayed so ops can distinguish
“active attack” (replay) from “infrastructure issue” (cache down).
SessionRevoked
M36: (sub, sid) row absent from user_sessions — the session
was revoked. STANDARDS_JWT_DETAILS_MITIGATION §E “row deletion =
revocation” — this is the textbook stateful-revocation gate that
makes the system “stateful by design” (OVERVIEW §6: stateless 환상 폐기). Distinct from SessionVersionStale (account-wide
epoch) because this axis kicks one device while leaving the
account’s other sessions alive.
M36 (substrate transient): session-row lookup substrate is unreachable. Engine fails closed.
SessionVersionStale
sv-port (Phase 5): token’s sv claim is strictly less than the
current per-account session_version. Break-glass / LogoutAll
bump current_sv inside the substrate; tokens minted before the
bump fail this gate within the cache TTL (60s default — see
SV_CACHE_TTL). PAS-internal callers preemptively flip
sv:{ppnum_id}; remote consumers (PCS chat-auth, pas-external
SDK) converge via the cache TTL.
sv-port (substrate transient): EpochRevocation::current failed.
Engine fails closed.
NotJwsCompact
Token cannot be split into a JWS Compact form (3 segments).
HeaderUnparseable
Header segment cannot be base64url-decoded or is not valid JSON.
PayloadUnparseable
Payload segment cannot be base64url-decoded or is not valid JSON.
Mirrors HeaderUnparseable for the second segment — structural,
not an M-row enforcement.
Trait Implementations§
Source§impl Error for AuthError
impl Error for AuthError
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()