Skip to main content

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}