ppoppo_token/access_token/verify_config.rs
1//! Per-request verification configuration for the RFC 9068 access-token
2//! profile.
3//!
4//! ── Composition ────────────────────────────────────────────────────────
5//!
6//! JOSE-shared fields (`issuer`, `audience`, `expected_typ`,
7//! `max_token_size`, `algorithms`) live in `engine::SharedVerifyConfig`
8//! and are reached via `self.shared`. Access-token-specific axes
9//! (replay/session/epoch revocation ports) stay on this struct. The
10//! engine submodules `check_algorithm` / `check_header` / `raw` read
11//! only from `&SharedVerifyConfig`; `check_claims` / `check_domain` /
12//! revocation checks read from this struct (and reach the shared
13//! fields via `cfg.shared.*`).
14//!
15//! ── Phase 5 — orthogonal port slots ────────────────────────────────────
16//!
17//! Replay / session ports model orthogonal revocation axes (M35-M38).
18//! Each is `Option<Arc<dyn ...>>` so callers wire only what their
19//! deployment substrate supports — `None` short-circuits the gate
20//! (legacy admit / sibling-test config / migration phases).
21//!
22//! The **epoch axis is different** (RFC_202607150428 §7 Q3): its slot is
23//! a required [`EpochEnforcement`] typed stance, not an `Option` — the
24//! `epoch: None` silent default is what let four shipped consumers skip
25//! the only mechanism that can kill a live access token. Constructing a
26//! `VerifyConfig` without declaring the stance no longer compiles.
27
28use std::sync::Arc;
29
30use super::epoch_revocation::EpochEnforcement;
31use super::replay_defense::ReplayDefense;
32use super::session_revocation::SessionRevocation;
33use crate::algorithm::Algorithm;
34use crate::engine::shared_config::SharedVerifyConfig;
35
36#[derive(Debug, Clone)]
37#[allow(dead_code)] // ports consumed across Phase 5+
38pub struct VerifyConfig {
39 pub(crate) shared: SharedVerifyConfig,
40
41 // ── Phase 5 revocation port slots ──────────────────────────────────
42 /// M35 jti replay defense (Phase 5 commit 5.1).
43 pub(crate) replay: Option<Arc<dyn ReplayDefense>>,
44 /// M36 session-row liveness (Phase 5 commit 5.2).
45 pub(crate) session: Option<Arc<dyn SessionRevocation>>,
46 /// sv-axis typed stance (required — RFC_202607150428 Q3).
47 pub(crate) epoch: EpochEnforcement,
48}
49
50impl VerifyConfig {
51 /// Canonical access-token config: `at+jwt` typ, sealed-vocabulary
52 /// algorithm whitelist (`Algorithm::ALL` — every alg PAS recognises;
53 /// EdDSA-only today), 8 KB token size cap (M34). Replay / session
54 /// port slots default to `None` (opt in via the builders); the sv
55 /// epoch stance is **required** — there is no silent default.
56 pub fn access_token(
57 issuer: impl Into<String>,
58 audience: impl Into<String>,
59 epoch: EpochEnforcement,
60 ) -> Self {
61 Self {
62 shared: SharedVerifyConfig::new(
63 issuer,
64 audience,
65 "at+jwt",
66 8 * 1024,
67 Algorithm::ALL.to_vec(),
68 ),
69 replay: None,
70 session: None,
71 epoch,
72 }
73 }
74
75 /// Override the algorithm whitelist. Test-only escape hatch — production
76 /// callers MUST go through `access_token` so the EdDSA pin is the default.
77 #[must_use]
78 pub fn with_algorithms(mut self, algorithms: Vec<Algorithm>) -> Self {
79 self.shared.algorithms = algorithms;
80 self
81 }
82
83 /// Wire the M35 jti replay defense port.
84 #[must_use]
85 pub fn with_replay_defense(mut self, port: Arc<dyn ReplayDefense>) -> Self {
86 self.replay = Some(port);
87 self
88 }
89
90 /// Wire the M36 session-row liveness port.
91 #[must_use]
92 pub fn with_session_revocation(mut self, port: Arc<dyn SessionRevocation>) -> Self {
93 self.session = Some(port);
94 self
95 }
96}