this repo has no description
1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
2use bspds::oauth::dpop::{DPoPVerifier, compute_jwk_thumbprint, DPoPJwk};
3use chrono::Utc;
4use serde_json::json;
5
6fn create_dpop_proof(
7 method: &str,
8 uri: &str,
9 nonce: Option<&str>,
10 ath: Option<&str>,
11 iat_offset_secs: i64,
12) -> String {
13 use p256::ecdsa::{SigningKey, Signature, signature::Signer};
14
15 let signing_key = SigningKey::random(&mut rand::thread_rng());
16 let verifying_key = signing_key.verifying_key();
17 let point = verifying_key.to_encoded_point(false);
18
19 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
20 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
21
22 let jwk = json!({
23 "kty": "EC",
24 "crv": "P-256",
25 "x": x,
26 "y": y
27 });
28
29 let header = json!({
30 "typ": "dpop+jwt",
31 "alg": "ES256",
32 "jwk": jwk
33 });
34
35 let mut payload = json!({
36 "jti": format!("unique-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)),
37 "htm": method,
38 "htu": uri,
39 "iat": Utc::now().timestamp() + iat_offset_secs
40 });
41
42 if let Some(n) = nonce {
43 payload["nonce"] = json!(n);
44 }
45
46 if let Some(a) = ath {
47 payload["ath"] = json!(a);
48 }
49
50 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
51 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
52
53 let signing_input = format!("{}.{}", header_b64, payload_b64);
54 let signature: Signature = signing_key.sign(signing_input.as_bytes());
55 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
56
57 format!("{}.{}", signing_input, signature_b64)
58}
59
60#[test]
61fn test_dpop_nonce_generation() {
62 let secret = b"test-dpop-secret-32-bytes-long!!";
63 let verifier = DPoPVerifier::new(secret);
64
65 let nonce1 = verifier.generate_nonce();
66 let nonce2 = verifier.generate_nonce();
67
68 assert!(!nonce1.is_empty());
69 assert!(!nonce2.is_empty());
70}
71
72#[test]
73fn test_dpop_nonce_validation_success() {
74 let secret = b"test-dpop-secret-32-bytes-long!!";
75 let verifier = DPoPVerifier::new(secret);
76
77 let nonce = verifier.generate_nonce();
78 let result = verifier.validate_nonce(&nonce);
79
80 assert!(result.is_ok(), "Valid nonce should pass: {:?}", result);
81}
82
83#[test]
84fn test_dpop_nonce_wrong_secret() {
85 let secret1 = b"test-dpop-secret-32-bytes-long!!";
86 let secret2 = b"different-secret-32-bytes-long!!";
87
88 let verifier1 = DPoPVerifier::new(secret1);
89 let verifier2 = DPoPVerifier::new(secret2);
90
91 let nonce = verifier1.generate_nonce();
92 let result = verifier2.validate_nonce(&nonce);
93
94 assert!(result.is_err(), "Nonce from different secret should fail");
95}
96
97#[test]
98fn test_dpop_nonce_invalid_format() {
99 let secret = b"test-dpop-secret-32-bytes-long!!";
100 let verifier = DPoPVerifier::new(secret);
101
102 assert!(verifier.validate_nonce("invalid").is_err());
103 assert!(verifier.validate_nonce("").is_err());
104 assert!(verifier.validate_nonce("!!!not-base64!!!").is_err());
105}
106
107#[test]
108fn test_jwk_thumbprint_ec_p256() {
109 let jwk = DPoPJwk {
110 kty: "EC".to_string(),
111 crv: Some("P-256".to_string()),
112 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()),
113 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()),
114 };
115
116 let thumbprint = compute_jwk_thumbprint(&jwk);
117 assert!(thumbprint.is_ok());
118
119 let tp = thumbprint.unwrap();
120 assert!(!tp.is_empty());
121 assert!(tp.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_'));
122}
123
124#[test]
125fn test_jwk_thumbprint_ec_secp256k1() {
126 let jwk = DPoPJwk {
127 kty: "EC".to_string(),
128 crv: Some("secp256k1".to_string()),
129 x: Some("some_x_value".to_string()),
130 y: Some("some_y_value".to_string()),
131 };
132
133 let thumbprint = compute_jwk_thumbprint(&jwk);
134 assert!(thumbprint.is_ok());
135}
136
137#[test]
138fn test_jwk_thumbprint_okp_ed25519() {
139 let jwk = DPoPJwk {
140 kty: "OKP".to_string(),
141 crv: Some("Ed25519".to_string()),
142 x: Some("some_x_value".to_string()),
143 y: None,
144 };
145
146 let thumbprint = compute_jwk_thumbprint(&jwk);
147 assert!(thumbprint.is_ok());
148}
149
150#[test]
151fn test_jwk_thumbprint_missing_crv() {
152 let jwk = DPoPJwk {
153 kty: "EC".to_string(),
154 crv: None,
155 x: Some("x".to_string()),
156 y: Some("y".to_string()),
157 };
158
159 let thumbprint = compute_jwk_thumbprint(&jwk);
160 assert!(thumbprint.is_err());
161}
162
163#[test]
164fn test_jwk_thumbprint_missing_x() {
165 let jwk = DPoPJwk {
166 kty: "EC".to_string(),
167 crv: Some("P-256".to_string()),
168 x: None,
169 y: Some("y".to_string()),
170 };
171
172 let thumbprint = compute_jwk_thumbprint(&jwk);
173 assert!(thumbprint.is_err());
174}
175
176#[test]
177fn test_jwk_thumbprint_missing_y_for_ec() {
178 let jwk = DPoPJwk {
179 kty: "EC".to_string(),
180 crv: Some("P-256".to_string()),
181 x: Some("x".to_string()),
182 y: None,
183 };
184
185 let thumbprint = compute_jwk_thumbprint(&jwk);
186 assert!(thumbprint.is_err());
187}
188
189#[test]
190fn test_jwk_thumbprint_unsupported_key_type() {
191 let jwk = DPoPJwk {
192 kty: "RSA".to_string(),
193 crv: None,
194 x: None,
195 y: None,
196 };
197
198 let thumbprint = compute_jwk_thumbprint(&jwk);
199 assert!(thumbprint.is_err());
200}
201
202#[test]
203fn test_jwk_thumbprint_deterministic() {
204 let jwk = DPoPJwk {
205 kty: "EC".to_string(),
206 crv: Some("P-256".to_string()),
207 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()),
208 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()),
209 };
210
211 let tp1 = compute_jwk_thumbprint(&jwk).unwrap();
212 let tp2 = compute_jwk_thumbprint(&jwk).unwrap();
213
214 assert_eq!(tp1, tp2, "Thumbprint should be deterministic");
215}
216
217#[test]
218fn test_dpop_proof_invalid_format() {
219 let secret = b"test-dpop-secret-32-bytes-long!!";
220 let verifier = DPoPVerifier::new(secret);
221
222 let result = verifier.verify_proof("not.enough.parts", "POST", "https://example.com", None);
223 assert!(result.is_err());
224
225 let result = verifier.verify_proof("invalid", "POST", "https://example.com", None);
226 assert!(result.is_err());
227}
228
229#[test]
230fn test_dpop_proof_invalid_typ() {
231 let secret = b"test-dpop-secret-32-bytes-long!!";
232 let verifier = DPoPVerifier::new(secret);
233
234 let header = json!({
235 "typ": "JWT",
236 "alg": "ES256",
237 "jwk": {
238 "kty": "EC",
239 "crv": "P-256",
240 "x": "x",
241 "y": "y"
242 }
243 });
244
245 let payload = json!({
246 "jti": "unique",
247 "htm": "POST",
248 "htu": "https://example.com",
249 "iat": Utc::now().timestamp()
250 });
251
252 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
253 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
254 let proof = format!("{}.{}.sig", header_b64, payload_b64);
255
256 let result = verifier.verify_proof(&proof, "POST", "https://example.com", None);
257 assert!(result.is_err());
258}
259
260#[test]
261fn test_dpop_proof_method_mismatch() {
262 let secret = b"test-dpop-secret-32-bytes-long!!";
263 let verifier = DPoPVerifier::new(secret);
264
265 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0);
266
267 let result = verifier.verify_proof(&proof, "GET", "https://example.com/token", None);
268 assert!(result.is_err());
269}
270
271#[test]
272fn test_dpop_proof_uri_mismatch() {
273 let secret = b"test-dpop-secret-32-bytes-long!!";
274 let verifier = DPoPVerifier::new(secret);
275
276 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0);
277
278 let result = verifier.verify_proof(&proof, "POST", "https://other.com/token", None);
279 assert!(result.is_err());
280}
281
282#[test]
283fn test_dpop_proof_iat_too_old() {
284 let secret = b"test-dpop-secret-32-bytes-long!!";
285 let verifier = DPoPVerifier::new(secret);
286
287 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, -600);
288
289 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None);
290 assert!(result.is_err());
291}
292
293#[test]
294fn test_dpop_proof_iat_future() {
295 let secret = b"test-dpop-secret-32-bytes-long!!";
296 let verifier = DPoPVerifier::new(secret);
297
298 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 600);
299
300 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None);
301 assert!(result.is_err());
302}
303
304#[test]
305fn test_dpop_proof_ath_mismatch() {
306 let secret = b"test-dpop-secret-32-bytes-long!!";
307 let verifier = DPoPVerifier::new(secret);
308
309 let proof = create_dpop_proof(
310 "GET",
311 "https://example.com/resource",
312 None,
313 Some("wrong_hash"),
314 0,
315 );
316
317 let result = verifier.verify_proof(
318 &proof,
319 "GET",
320 "https://example.com/resource",
321 Some("correct_hash"),
322 );
323 assert!(result.is_err());
324}
325
326#[test]
327fn test_dpop_proof_missing_ath_when_required() {
328 let secret = b"test-dpop-secret-32-bytes-long!!";
329 let verifier = DPoPVerifier::new(secret);
330
331 let proof = create_dpop_proof("GET", "https://example.com/resource", None, None, 0);
332
333 let result = verifier.verify_proof(
334 &proof,
335 "GET",
336 "https://example.com/resource",
337 Some("expected_hash"),
338 );
339 assert!(result.is_err());
340}
341
342#[test]
343fn test_dpop_proof_uri_ignores_query_params() {
344 let secret = b"test-dpop-secret-32-bytes-long!!";
345 let verifier = DPoPVerifier::new(secret);
346
347 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0);
348
349 let result = verifier.verify_proof(
350 &proof,
351 "POST",
352 "https://example.com/token?foo=bar",
353 None,
354 );
355
356 assert!(result.is_ok(), "Query params should be ignored: {:?}", result);
357}