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