ppoppo_token/id_token/issue_error.rs
1//! Issuance errors for the OIDC id_token engine.
2//!
3//! Mirror of `access_token::IssueError` shape: 1 variant per named failure
4//! mode so audit logs read the cause off the variant name without a lookup
5//! table (see `project_jwt_phase2_design` Decision 2). Variants are
6//! disjoint from the access-token enum because the failure modes don't
7//! overlap (id_token has no `KeyParse` because key construction lives in
8//! `crate::SigningKey` shared between profiles; the access-token enum
9//! retains it for legacy reasons).
10//!
11//! ── Why a separate enum from `access_token::IssueError` ─────────────────
12//!
13//! Same reasoning as `id_token::AuthError` vs `access_token::AuthError`:
14//! collapsing both into one enum forces every variant to carry "applies
15//! to which profile?" metadata, when the carrying enum's *type* already
16//! tells the reader which profile rejected. Profile-disjoint enums let
17//! each surface stay narrow (only id-token-specific failure modes
18//! enumerated here) while reusing the shared engine primitives
19//! (`SigningKey`, `KeySet`, `Algorithm`).
20
21/// id_token-specific issuance failure modes (Phase 10.10).
22#[derive(Debug, thiserror::Error, PartialEq, Eq)]
23pub enum IssueError {
24 /// `IssueConfig.kid` does not match the kid associated with the
25 /// supplied `SigningKey`. Emitted before any encoding work happens
26 /// so a misconfigured pipeline fails closed instead of issuing
27 /// id_tokens against an unrecognized kid. Mirrors
28 /// `access_token::IssueError::KeyMismatch` exactly — the engine's
29 /// kid-matching pre-flight is profile-agnostic, but each profile
30 /// owns its own variant so audit logs don't have to disambiguate
31 /// "kid mismatch on which profile".
32 #[error("issue: cfg kid '{cfg_kid}' does not match signer kid '{signer_kid}'")]
33 KeyMismatch { cfg_kid: String, signer_kid: String },
34
35 /// `jsonwebtoken::encode` failed at the JSON serialization step. In
36 /// practice this only fires when an `IssueRequest<S>` field overflows
37 /// the JSON number range — the registered claims are all integers
38 /// within `i64`, so production paths cannot hit it. Listed for
39 /// completeness so the engine never panics on serialization.
40 #[error("issue: failed to encode id_token JWT ({0})")]
41 JsonEncode(String),
42
43 /// System clock is before UNIX_EPOCH. Cannot happen on a correctly
44 /// configured machine; surfaces only on hardware-reset / NTP-broken
45 /// edge cases. Listed for completeness so the engine refuses to
46 /// emit garbage timestamps rather than panicking.
47 #[error("issue: system clock is before UNIX_EPOCH")]
48 ClockBackwards,
49
50 /// β1 runtime allowlist guard (Phase 10.10) — `IssueRequest<S>`
51 /// carries a populated PII field whose wire name is NOT in
52 /// `S::names()`. The HasX-gated builders prevent this at the *typed*
53 /// API surface (calling `.with_email(...)` on an `IssueRequest<Openid>`
54 /// is a compile error); this variant is the *runtime* mirror —
55 /// fires when intra-crate code bypasses the builders via
56 /// `pub(crate)` struct-literal access and pushes a PII field through
57 /// anyway. Symmetric to verify-side M72 (`AuthError::UnknownClaim`)
58 /// — the engine refuses to *emit* a claim it would refuse to
59 /// *accept*. Carries the offending wire name so audit logs see WHICH
60 /// claim tripped the gate.
61 #[error("issue: emission disallowed for claim '{0}' at this scope")]
62 EmissionDisallowed(String),
63}