authplane_sdk/
inbound_dpop.rs1use std::borrow::Cow;
15use std::sync::Arc;
16
17use jsonwebtoken::Algorithm;
18
19use crate::dpop_replay::{DpopReplayStore, InMemoryDpopReplayStore};
20
21#[derive(Clone, Debug)]
29pub struct InboundDPoPOptions {
30 required: bool,
31 allowed_proof_algorithms: Option<Vec<Algorithm>>,
32 max_proof_age_seconds: Option<u64>,
33 clock_skew_seconds: Option<u64>,
34 replay_store: Arc<dyn DpopReplayStore>,
43}
44
45impl Default for InboundDPoPOptions {
46 fn default() -> Self {
47 Self {
48 required: false,
49 allowed_proof_algorithms: None,
50 max_proof_age_seconds: None,
51 clock_skew_seconds: None,
52 replay_store: Arc::new(InMemoryDpopReplayStore::new()),
53 }
54 }
55}
56
57impl InboundDPoPOptions {
58 pub fn required() -> Self {
62 Self {
63 required: true,
64 ..Self::default()
65 }
66 }
67
68 pub fn with_required(mut self, required: bool) -> Self {
72 self.required = required;
73 self
74 }
75
76 pub fn with_allowed_proof_algorithms(
82 mut self,
83 algorithms: Vec<Algorithm>,
84 ) -> Result<Self, InboundDPoPOptionsError> {
85 if algorithms.is_empty() {
86 return Err(InboundDPoPOptionsError::EmptyAlgorithmList);
87 }
88 for alg in &algorithms {
89 if !crate::dpop::SUPPORTED_DPOP_ALGORITHMS.contains(alg) {
90 return Err(InboundDPoPOptionsError::UnsupportedAlgorithm(*alg));
91 }
92 }
93 self.allowed_proof_algorithms = Some(algorithms);
94 Ok(self)
95 }
96
97 pub fn with_max_proof_age_seconds(mut self, seconds: u64) -> Self {
100 self.max_proof_age_seconds = Some(seconds);
101 self
102 }
103
104 pub fn with_clock_skew_seconds(mut self, seconds: u64) -> Self {
107 self.clock_skew_seconds = Some(seconds);
108 self
109 }
110
111 pub fn with_replay_store(mut self, store: Arc<dyn DpopReplayStore>) -> Self {
118 self.replay_store = store;
119 self
120 }
121
122 pub fn is_required(&self) -> bool {
124 self.required
125 }
126
127 pub fn replay_store(&self) -> &Arc<dyn DpopReplayStore> {
131 &self.replay_store
132 }
133
134 pub fn resolved_allowed_proof_algorithms(&self) -> Cow<'_, [Algorithm]> {
138 match self.allowed_proof_algorithms.as_deref() {
139 Some(algs) => Cow::Borrowed(algs),
140 None => Cow::Borrowed(crate::dpop::SUPPORTED_DPOP_ALGORITHMS),
141 }
142 }
143
144 pub fn resolved_max_proof_age_seconds(&self, resource_default: u64) -> u64 {
149 self.max_proof_age_seconds.unwrap_or(resource_default)
150 }
151
152 pub fn resolved_clock_skew_seconds(&self, resource_default: u64) -> u64 {
156 self.clock_skew_seconds.unwrap_or(resource_default)
157 }
158}
159
160#[derive(Debug, thiserror::Error)]
162#[non_exhaustive]
163pub enum InboundDPoPOptionsError {
164 #[error(
165 "allowed_proof_algorithms must be non-empty; omit the setter to accept the default set"
166 )]
167 EmptyAlgorithmList,
168
169 #[error("DPoP proof algorithm {0:?} is not supported; supported algorithms are ES256, RS256")]
170 UnsupportedAlgorithm(Algorithm),
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[tokio::test]
178 async fn defaults_to_mode_2() {
179 let opts = InboundDPoPOptions::default();
180 assert!(!opts.is_required());
181 assert_eq!(opts.resolved_max_proof_age_seconds(600), 600);
183 assert_eq!(opts.resolved_clock_skew_seconds(60), 60);
184 let stored = opts
189 .replay_store()
190 .check_and_store("jti-default-test", 4102444800)
191 .await
192 .expect("default replay store must be usable");
193 assert!(stored, "first observation of jti must succeed");
194 }
195
196 #[tokio::test]
197 async fn each_default_allocates_an_independent_replay_store() {
198 let opts_a = InboundDPoPOptions::default();
202 let opts_b = InboundDPoPOptions::default();
203 opts_a
204 .replay_store()
205 .check_and_store("jti-shared", 4102444800)
206 .await
207 .expect("store A accepts");
208 let stored_in_b = opts_b
209 .replay_store()
210 .check_and_store("jti-shared", 4102444800)
211 .await
212 .expect("store B accepts");
213 assert!(stored_in_b, "distinct defaults must not share replay state");
214 }
215
216 #[test]
217 fn required_shortcut_sets_only_the_flag() {
218 let opts = InboundDPoPOptions::required();
219 assert!(opts.is_required());
220 assert_eq!(opts.resolved_clock_skew_seconds(60), 60);
222 }
223
224 #[test]
225 fn with_allowed_proof_algorithms_rejects_empty() {
226 let res = InboundDPoPOptions::default().with_allowed_proof_algorithms(vec![]);
227 assert!(matches!(
228 res,
229 Err(InboundDPoPOptionsError::EmptyAlgorithmList)
230 ));
231 }
232
233 #[test]
234 fn with_allowed_proof_algorithms_rejects_unsupported() {
235 let res =
236 InboundDPoPOptions::default().with_allowed_proof_algorithms(vec![Algorithm::HS256]);
237 assert!(matches!(
238 res,
239 Err(InboundDPoPOptionsError::UnsupportedAlgorithm(
240 Algorithm::HS256
241 ))
242 ));
243 }
244
245 #[test]
246 fn with_allowed_proof_algorithms_accepts_es256_subset() {
247 let opts = InboundDPoPOptions::default()
248 .with_allowed_proof_algorithms(vec![Algorithm::ES256])
249 .expect("valid");
250 match opts.resolved_allowed_proof_algorithms() {
251 Cow::Borrowed(slice) => assert_eq!(slice, &[Algorithm::ES256]),
252 Cow::Owned(_) => panic!("setter slice should be borrowed"),
253 }
254 }
255
256 #[test]
257 fn resolved_allowed_proof_algorithms_borrows_default_when_unset() {
258 let opts = InboundDPoPOptions::default();
259 let resolved = opts.resolved_allowed_proof_algorithms();
260 assert!(matches!(resolved, Cow::Borrowed(_)));
262 assert_eq!(&*resolved, crate::dpop::SUPPORTED_DPOP_ALGORITHMS);
263 }
264
265 #[test]
266 fn resolved_clock_skew_inherits_resource_default() {
267 let opts = InboundDPoPOptions::default();
268 assert_eq!(opts.resolved_clock_skew_seconds(60), 60);
269 let opts = opts.with_clock_skew_seconds(10);
270 assert_eq!(opts.resolved_clock_skew_seconds(60), 10);
271 }
272
273 #[test]
274 fn resolved_max_proof_age_inherits_resource_default() {
275 let opts = InboundDPoPOptions::default();
276 assert_eq!(opts.resolved_max_proof_age_seconds(600), 600);
277 let opts = opts.with_max_proof_age_seconds(120);
278 assert_eq!(opts.resolved_max_proof_age_seconds(600), 120);
279 }
280}