Skip to main content

ppoppo_token/id_token/
verify_config.rs

1//! Per-request id_token verification configuration.
2//!
3//! ── Composition ────────────────────────────────────────────────────────
4//!
5//! JOSE-shared fields (`issuer`, `audience`, `expected_typ`,
6//! `max_token_size`, `algorithms`) live in `engine::SharedVerifyConfig`
7//! and are reached via `self.shared`. OIDC-specific axes
8//! (`expected_nonce`, `max_age`, `acr_values`) stay on this struct.
9//! Symmetric to `access_token::VerifyConfig`'s replay/session/epoch
10//! ports — neither profile imports the other's policy axes.
11//!
12//! ── Required vs Phase-deferred fields ───────────────────────────────────
13//!
14//! * `expected_nonce` — required at construction. M66 binding is *not*
15//!   opt-in: the OIDC profile mandates nonce when the RP sent one in
16//!   the auth request, and the engine refuses to verify without one to
17//!   prevent a footgun where a misconfigured config silently skips the
18//!   gate.
19//! * `max_age` / `acr_values` — `Option<...>` placeholders. Phase 10.6
20//!   (M70) and 10.7 (M71) wire their consumption; until then they're
21//!   inert. Pre-pinning the field shape now means those rows are
22//!   commits that *populate*, not commits that *re-shape* the struct.
23
24use super::Nonce;
25use crate::algorithm::Algorithm;
26use crate::engine::shared_config::SharedVerifyConfig;
27
28#[derive(Debug, Clone)]
29#[allow(dead_code)] // max_age/acr_values consumed in Phase 10.6/10.7
30pub struct VerifyConfig {
31    pub(crate) shared: SharedVerifyConfig,
32
33    /// M66 — RP-stored nonce. Required field; construction goes through
34    /// `VerifyConfig::id_token(...)` which takes a `Nonce`.
35    pub(crate) expected_nonce: Nonce,
36
37    /// M67 — when `Some(token)`, the engine compares the payload's
38    /// `at_hash` claim against `SHA-256(token)[..16]` base64url-no-pad.
39    /// Set via `with_access_token_binding`; populated by RPs that
40    /// receive both id_token and access_token in the same response
41    /// (hybrid + implicit flows, OIDC Core §3.1.3.8). When `None`, the
42    /// engine does not inspect `at_hash` — pure code flow consumers
43    /// (RCW/CTW today) leave this unset.
44    pub(crate) expected_access_token: Option<String>,
45
46    /// M68 — when `Some(code)`, the engine compares the payload's
47    /// `c_hash` claim against `SHA-256(code)[..16]` base64url-no-pad.
48    /// Set via `with_authorization_code_binding`; populated by RPs that
49    /// receive both id_token and authorization_code in the same response
50    /// (hybrid flow, OIDC Core §3.3.2.11). When `None`, the engine does
51    /// not inspect `c_hash`.
52    pub(crate) expected_authorization_code: Option<String>,
53
54    /// Phase 10.6 (M70) — when `Some(n)`, refuses tokens whose
55    /// `auth_time` is more than `n` seconds in the past.
56    pub(crate) max_age: Option<i64>,
57
58    /// Phase 10.7 (M71) — when `Some(values)`, refuses tokens whose
59    /// `acr` is not in `values`.
60    pub(crate) acr_values: Option<Vec<String>>,
61}
62
63impl VerifyConfig {
64    /// Canonical id_token config: `JWT` typ (distinct from access
65    /// token's `at+jwt`), EdDSA-only algorithm whitelist, 8 KB token
66    /// size cap, RP-stored `expected_nonce` required.
67    pub fn id_token(
68        issuer: impl Into<String>,
69        audience: impl Into<String>,
70        expected_nonce: Nonce,
71    ) -> Self {
72        Self {
73            shared: SharedVerifyConfig::new(
74                issuer,
75                audience,
76                "JWT",
77                8 * 1024,
78                vec![Algorithm::EdDSA],
79            ),
80            expected_nonce,
81            expected_access_token: None,
82            expected_authorization_code: None,
83            max_age: None,
84            acr_values: None,
85        }
86    }
87
88    /// Override the algorithm whitelist. Test-only escape hatch —
89    /// production callers MUST go through `id_token` so EdDSA is the
90    /// default, not an override.
91    #[must_use]
92    pub fn with_algorithms(mut self, algorithms: Vec<Algorithm>) -> Self {
93        self.shared.algorithms = algorithms;
94        self
95    }
96
97    /// M67 — bind verification to the access_token the RP just received.
98    /// On verify, the engine asserts
99    /// `payload.at_hash == base64url(SHA-256(access_token)[..16])`.
100    /// Required when the response carries both id_token and access_token
101    /// (hybrid flow `code id_token token`, implicit `id_token token`).
102    /// Pure code flow consumers do not call this.
103    #[must_use]
104    pub fn with_access_token_binding(mut self, access_token: impl Into<String>) -> Self {
105        self.expected_access_token = Some(access_token.into());
106        self
107    }
108
109    /// M68 — bind verification to the authorization code the RP just
110    /// received at the redirect_uri. On verify, the engine asserts
111    /// `payload.c_hash == base64url(SHA-256(code)[..16])`. Required when
112    /// the hybrid response carries both id_token and code
113    /// (`code id_token`, `code id_token token`). Pure implicit-flow
114    /// consumers do not call this.
115    #[must_use]
116    pub fn with_authorization_code_binding(mut self, code: impl Into<String>) -> Self {
117        self.expected_authorization_code = Some(code.into());
118        self
119    }
120
121    /// Phase 10.6 (M70) wire site. Inert in 10.1.C.
122    #[must_use]
123    pub fn with_max_age(mut self, max_age: i64) -> Self {
124        self.max_age = Some(max_age);
125        self
126    }
127
128    /// Phase 10.7 (M71) wire site. Inert in 10.1.C.
129    #[must_use]
130    pub fn with_acr_values(mut self, acr_values: Vec<String>) -> Self {
131        self.acr_values = Some(acr_values);
132        self
133    }
134}