Skip to main content

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}