ppoppo_token/access_token/error.rs
1//! Verification errors for the RFC 9068 access-token profile.
2//!
3//! ── Surface composition ─────────────────────────────────────────────────
4//!
5//! JOSE wire-format errors (M01-M16a algorithm/header, M31-M34
6//! serialization, parse-shape failures) live in
7//! `engine::SharedAuthError` and reach this enum via the
8//! [`AuthError::Jose`] carrier variant — the engine submodules emit
9//! `SharedAuthError`, the access-token verify path re-wraps. This is a
10//! deliberate seam: it lets each profile (access vs OIDC id_token)
11//! enumerate only its profile-specific variants while sharing the JOSE
12//! check pipeline structurally.
13//!
14//! Profile-specific variants enumerated here:
15//! * **C. Claims (M17-M30)** — RFC 9068 / 7519 registered-claim
16//! semantics. Includes `cat` (M29) and `client_id` (M28a) which OIDC
17//! id_tokens do not carry.
18//! * **F. Domain (M39-M45)** — ppoppo-specific domain rules.
19//! * **E. Replay / Revocation (M35-M38 + sv)** — Phase 5 ports.
20//!
21//! Variants map 1:1 to mitigation IDs in
22//! `0context/STANDARDS_JWT_DETAILS_MITIGATION_PPOPPO.md` §C, §F, §E.
23//! Adding a mitigation row here without a variant — or moving a variant
24//! between this enum and `SharedAuthError` — is a drift signal for the
25//! Standards-Sync table.
26
27use crate::engine::shared_error::SharedAuthError;
28
29#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
30pub enum AuthError {
31 /// JOSE-shared wire-format errors (algorithm / header / serialization
32 /// / parse). The engine submodules `check_algorithm` / `check_header`
33 /// / `raw` and the structural rejections in `engine::verify` all
34 /// surface through this carrier.
35 #[error(transparent)]
36 Jose(#[from] SharedAuthError),
37
38 // ── C. Claims (M17–M30) ───────────────────────────────────────────────
39 #[error("M17: exp claim missing")]
40 ExpMissing,
41 #[error("M18: token expired")]
42 Expired,
43 #[error("M19: exp exceeds upper bound")]
44 ExpUpperBound,
45 #[error("M20: aud claim missing")]
46 AudMissing,
47 /// M21 + M22 collapsed (string vs array form) — the audit signal is
48 /// identical: the audience binding does not match this verifier.
49 #[error("M21/M22: aud value does not match expected audience")]
50 AudMismatch,
51 /// M23: `iss` is missing OR does not match the pinned issuer.
52 /// Single variant — the audit signal is "this token did not come
53 /// from the trusted issuer".
54 #[error("M23: iss missing or does not match pinned issuer")]
55 IssMismatch,
56 #[error("M24: iat claim missing")]
57 IatMissing,
58 /// M24 (future-iat) + M25 collapsed — both enforce `iat > now + 60s`.
59 #[error("M24/M25: iat is in the future beyond 60s leeway")]
60 IatFuture,
61 #[error("M26: nbf is in the future — token not yet valid")]
62 NotYetValid,
63 #[error("M27: jti claim missing")]
64 JtiMissing,
65 #[error("M28: sub claim missing")]
66 SubMissing,
67 /// M28a: RFC 9068 §2.2 — access JWTs MUST carry `client_id`.
68 #[error("M28a: client_id claim missing")]
69 ClientIdMissing,
70 /// M29: `cat` (token category) discriminator. RFC 9068 §2.2 + ppoppo
71 /// extension.
72 #[error("M29: cat does not match expected token type")]
73 TokenTypeMismatch,
74 /// M30: numeric claim is not a JSON integer.
75 #[error("M30: numeric claim is not a JSON integer")]
76 InvalidNumericType,
77
78 // ── F. Domain (M39–M45) ───────────────────────────────────────────────
79 #[error("M39: sub is not a valid ULID")]
80 SubFormatInvalid,
81 #[error("M40: account_type outside whitelist")]
82 AccountTypeInvalid,
83 #[error("M41: caps is not a JSON array of strings")]
84 CapsShapeInvalid,
85 #[error("M42: scopes is not a JSON array of strings")]
86 ScopesShapeInvalid,
87 #[error("M42: scopes exceeds 256-entry cap")]
88 ScopesTooLong,
89 #[error("M43: dlg_depth invalid (non-integer, negative, or > 4)")]
90 DlgDepthInvalid,
91 #[error("M44: admin claim requires active_ppnum in admin band")]
92 AdminBandRejected,
93 /// M45: payload contains a claim outside the engine's strict
94 /// allowlist. The variant carries the offending claim name for audit.
95 #[error("M45: unknown claim '{0}'")]
96 UnknownClaim(String),
97
98 // ── E. Replay / revocation (M35–M38 + sv) — Phase 5 ────────────────────
99 #[error("M35: jti replayed within TTL")]
100 JtiReplayed,
101 #[error("M35: replay cache substrate unavailable")]
102 ReplayCacheUnavailable,
103 #[error("M36: session revoked (user_sessions row absent)")]
104 SessionRevoked,
105 #[error("M36: session lookup substrate unavailable")]
106 SessionLookupUnavailable,
107 #[error("session_version stale: token < current epoch")]
108 SessionVersionStale,
109 #[error("session_version lookup substrate unavailable")]
110 SessionVersionLookupUnavailable,
111}