Skip to main content

ppoppo_sdk_core/verifier/
error.rs

1//! `VerifyError` — verifier crypto-side failure surface.
2//!
3//! One variant per logical failure class. The PAS-engine variants
4//! (`SignatureInvalid`, `Expired`, `IssuerInvalid`, `AudienceInvalid`,
5//! `MissingClaim`, `KeysetUnavailable`) reflect the boundary contract:
6//! audit logs map them 1:1 to engine `AuthError` rows. Adapter-side
7//! variants (`InvalidFormat`) cover failures upstream of engine entry.
8
9#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
10pub enum VerifyError {
11    /// Bearer string did not parse as a JWS Compact serialization.
12    /// Adapter-side reject before engine entry.
13    #[error("invalid bearer token format")]
14    InvalidFormat,
15
16    /// Cryptographic signature verification failed (engine M16).
17    #[error("signature verification failed")]
18    SignatureInvalid,
19
20    /// `exp` claim is in the past (engine M19).
21    #[error("token expired")]
22    Expired,
23
24    /// `iss` did not match `super::VerifyConfig::issuer` (engine M23). The
25    /// engine does not expose the *actual* value because the failed
26    /// match means we cannot trust any payload field — the SDK
27    /// surfaces just "issuer invalid" and the audit log carries the
28    /// caller's expected value alongside this variant.
29    #[error("issuer invalid (M23)")]
30    IssuerInvalid,
31
32    /// `aud` did not match `super::VerifyConfig::audience` (engine M21/M22).
33    #[error("audience invalid (M21/M22)")]
34    AudienceInvalid,
35
36    /// A required claim was absent or malformed.
37    #[error("missing required claim: {0}")]
38    MissingClaim(&'static str),
39
40    /// JWKS fetch failed and there is no usable cached snapshot
41    /// (initial bootstrap failure or `with_initial` constructed with
42    /// an empty key set). Distinct from `SignatureInvalid` so audit
43    /// logs distinguish "we couldn't even attempt verification" from
44    /// "verification failed."
45    #[error("keyset unavailable")]
46    KeysetUnavailable,
47
48    /// Engine `check_epoch` rejected: token's `sv` claim is below the
49    /// authoritative substrate's current value. Break-glass /
50    /// LogoutAll just kicked this token (RFC §3 Row 3 + STANDARDS_AUTH_INVALIDATION
51    /// §2.3). Distinct from `Expired` (which is `exp`-bound) — the
52    /// caller's UX response is the same (re-authenticate) but audit
53    /// logs distinguish revocation from natural expiry.
54    ///
55    /// Surfaces only when the consumer wired
56    /// `JwtVerifier::with_epoch_revocation` at boot. With no port
57    /// wired, the engine's epoch gate short-circuits and this variant
58    /// is unreachable.
59    #[error("session_version stale (engine epoch port reject)")]
60    SessionVersionStale,
61
62    /// Engine `check_epoch` could not reach its substrate (cache miss
63    /// fell through to fetcher; fetcher returned transient error).
64    /// Fail-closed per STANDARDS_AUTH_INVALIDATION §3 — admit-on-
65    /// failure would let stale tokens slip during outage windows.
66    /// Caller's HTTP response should be `503 Service Unavailable`.
67    ///
68    /// Surfaces only when the consumer wired
69    /// `JwtVerifier::with_epoch_revocation` at boot.
70    #[error("session_version lookup substrate unavailable")]
71    SessionVersionLookupUnavailable,
72
73    /// Phase 11.Z 0.10.0 (RFC_2026-05-08 §4.2 lock) — L2 session
74    /// liveness reject. The token's `sid` claim resolved to a row that
75    /// is absent OR `revoked_at` is set. Distinct from
76    /// `SessionVersionStale` (L1 sv-axis): L2 is consumer-DB row
77    /// revocation; L1 is cross-service break-glass propagation.
78    /// Caller's HTTP response is `401` and the browser cookie clears
79    /// (the `LogoutAll`/per-session-revoke flow's actionable signal).
80    ///
81    /// Surfaces only when the consumer wired
82    /// `JwtVerifier::with_session_liveness` at boot AND the token
83    /// carries a `sid` claim. Tokens without `sid` (machine
84    /// credentials, AI-agent flows, R6 legacy admit per
85    /// `super::VerifiedClaims::session_id`) admit without consulting
86    /// the L2 port (lenient — RFC_2026-05-08 §4.2 lock).
87    #[error("session revoked")]
88    SessionRevoked,
89
90    /// Phase 11.Z 0.10.0 — L2 session liveness substrate could not
91    /// answer (DB connection lost, schema unavailable, query timeout).
92    /// Fail-closed per STANDARDS_AUTH_INVALIDATION §3 — admit-on-
93    /// failure would let post-revoke tokens slip through during outage
94    /// windows. Caller's HTTP response should be `503 Service
95    /// Unavailable`. Distinct from `SessionVersionLookupUnavailable`
96    /// (L1) so audit dashboards pivot L1 substrate health vs L2
97    /// substrate health independently.
98    ///
99    /// Surfaces only when the consumer wired
100    /// `JwtVerifier::with_session_liveness` at boot.
101    #[error("session liveness lookup substrate unavailable")]
102    SessionLivenessLookupUnavailable,
103
104    /// M73 — id_token presented as a Bearer token. RFC 9068 §1 (negative)
105    /// + OIDC Core §1.2 intent: id_tokens authenticate the user *to the
106    /// RP*; access_tokens authorize the RP *to the resource server*.
107    /// The two are not interchangeable. Many 3rd-party RPs misuse
108    /// id_token for API access — the SDK's BearerVerifier surface is for
109    /// resource servers, so an id_token-shaped JWT here is always wrong.
110    ///
111    /// Distinct from `SignatureInvalid` (which is the engine's catch-all
112    /// for "token cannot be trusted") so audit logs distinguish a
113    /// developer-misuse signal ("you're sending the wrong token class")
114    /// from a forgery signal ("the signature didn't verify"). Rejected
115    /// BEFORE engine entry so the audit log does not get the same
116    /// signal masked by `TypMismatch → SignatureInvalid` collapsing.
117    #[error("M73: id_token presented as Bearer — use access_token for resource access")]
118    IdTokenAsBearer,
119
120    /// Catch-all for engine variants that don't map to a structural
121    /// SDK rejection. Carries the engine's `AuthError` Display so the
122    /// audit log retains the M-code.
123    #[error("verification failed: {0}")]
124    Other(String),
125}