A community based topic aggregation platform built on atproto
1package auth
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "crypto/sha256"
8 "encoding/base64"
9 "encoding/json"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/golang-jwt/jwt/v5"
15 "github.com/google/uuid"
16)
17
18// === Test Helpers ===
19
20// testECKey holds a test ES256 key pair
21type testECKey struct {
22 privateKey *ecdsa.PrivateKey
23 publicKey *ecdsa.PublicKey
24 jwk map[string]interface{}
25 thumbprint string
26}
27
28// generateTestES256Key generates a test ES256 key pair and JWK
29func generateTestES256Key(t *testing.T) *testECKey {
30 t.Helper()
31
32 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
33 if err != nil {
34 t.Fatalf("Failed to generate test key: %v", err)
35 }
36
37 // Encode public key coordinates as base64url
38 xBytes := privateKey.PublicKey.X.Bytes()
39 yBytes := privateKey.PublicKey.Y.Bytes()
40
41 // P-256 coordinates must be 32 bytes (pad if needed)
42 xBytes = padTo32Bytes(xBytes)
43 yBytes = padTo32Bytes(yBytes)
44
45 x := base64.RawURLEncoding.EncodeToString(xBytes)
46 y := base64.RawURLEncoding.EncodeToString(yBytes)
47
48 jwk := map[string]interface{}{
49 "kty": "EC",
50 "crv": "P-256",
51 "x": x,
52 "y": y,
53 }
54
55 // Calculate thumbprint
56 thumbprint, err := CalculateJWKThumbprint(jwk)
57 if err != nil {
58 t.Fatalf("Failed to calculate thumbprint: %v", err)
59 }
60
61 return &testECKey{
62 privateKey: privateKey,
63 publicKey: &privateKey.PublicKey,
64 jwk: jwk,
65 thumbprint: thumbprint,
66 }
67}
68
69// padTo32Bytes pads a byte slice to 32 bytes (required for P-256 coordinates)
70func padTo32Bytes(b []byte) []byte {
71 if len(b) >= 32 {
72 return b
73 }
74 padded := make([]byte, 32)
75 copy(padded[32-len(b):], b)
76 return padded
77}
78
79// createDPoPProof creates a DPoP proof JWT for testing
80func createDPoPProof(t *testing.T, key *testECKey, method, uri string, iat time.Time, jti string) string {
81 t.Helper()
82
83 claims := &DPoPClaims{
84 RegisteredClaims: jwt.RegisteredClaims{
85 ID: jti,
86 IssuedAt: jwt.NewNumericDate(iat),
87 },
88 HTTPMethod: method,
89 HTTPURI: uri,
90 }
91
92 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
93 token.Header["typ"] = "dpop+jwt"
94 token.Header["jwk"] = key.jwk
95
96 tokenString, err := token.SignedString(key.privateKey)
97 if err != nil {
98 t.Fatalf("Failed to create DPoP proof: %v", err)
99 }
100
101 return tokenString
102}
103
104// === JWK Thumbprint Tests (RFC 7638) ===
105
106func TestCalculateJWKThumbprint_EC_P256(t *testing.T) {
107 // Test with known values from RFC 7638 Appendix A (adapted for P-256)
108 jwk := map[string]interface{}{
109 "kty": "EC",
110 "crv": "P-256",
111 "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
112 "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE",
113 }
114
115 thumbprint, err := CalculateJWKThumbprint(jwk)
116 if err != nil {
117 t.Fatalf("CalculateJWKThumbprint failed: %v", err)
118 }
119
120 if thumbprint == "" {
121 t.Error("Expected non-empty thumbprint")
122 }
123
124 // Verify it's valid base64url
125 _, err = base64.RawURLEncoding.DecodeString(thumbprint)
126 if err != nil {
127 t.Errorf("Thumbprint is not valid base64url: %v", err)
128 }
129
130 // Verify length (SHA-256 produces 32 bytes = 43 base64url chars)
131 if len(thumbprint) != 43 {
132 t.Errorf("Expected thumbprint length 43, got %d", len(thumbprint))
133 }
134}
135
136func TestCalculateJWKThumbprint_Deterministic(t *testing.T) {
137 // Same key should produce same thumbprint
138 jwk := map[string]interface{}{
139 "kty": "EC",
140 "crv": "P-256",
141 "x": "test-x-coordinate",
142 "y": "test-y-coordinate",
143 }
144
145 thumbprint1, err := CalculateJWKThumbprint(jwk)
146 if err != nil {
147 t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
148 }
149
150 thumbprint2, err := CalculateJWKThumbprint(jwk)
151 if err != nil {
152 t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
153 }
154
155 if thumbprint1 != thumbprint2 {
156 t.Errorf("Thumbprints are not deterministic: %s != %s", thumbprint1, thumbprint2)
157 }
158}
159
160func TestCalculateJWKThumbprint_DifferentKeys(t *testing.T) {
161 // Different keys should produce different thumbprints
162 jwk1 := map[string]interface{}{
163 "kty": "EC",
164 "crv": "P-256",
165 "x": "coordinate-x-1",
166 "y": "coordinate-y-1",
167 }
168
169 jwk2 := map[string]interface{}{
170 "kty": "EC",
171 "crv": "P-256",
172 "x": "coordinate-x-2",
173 "y": "coordinate-y-2",
174 }
175
176 thumbprint1, err := CalculateJWKThumbprint(jwk1)
177 if err != nil {
178 t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
179 }
180
181 thumbprint2, err := CalculateJWKThumbprint(jwk2)
182 if err != nil {
183 t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
184 }
185
186 if thumbprint1 == thumbprint2 {
187 t.Error("Different keys produced same thumbprint (collision)")
188 }
189}
190
191func TestCalculateJWKThumbprint_MissingKty(t *testing.T) {
192 jwk := map[string]interface{}{
193 "crv": "P-256",
194 "x": "test-x",
195 "y": "test-y",
196 }
197
198 _, err := CalculateJWKThumbprint(jwk)
199 if err == nil {
200 t.Error("Expected error for missing kty, got nil")
201 }
202 if err != nil && !contains(err.Error(), "missing kty") {
203 t.Errorf("Expected error about missing kty, got: %v", err)
204 }
205}
206
207func TestCalculateJWKThumbprint_EC_MissingCrv(t *testing.T) {
208 jwk := map[string]interface{}{
209 "kty": "EC",
210 "x": "test-x",
211 "y": "test-y",
212 }
213
214 _, err := CalculateJWKThumbprint(jwk)
215 if err == nil {
216 t.Error("Expected error for missing crv, got nil")
217 }
218 if err != nil && !contains(err.Error(), "missing crv") {
219 t.Errorf("Expected error about missing crv, got: %v", err)
220 }
221}
222
223func TestCalculateJWKThumbprint_EC_MissingX(t *testing.T) {
224 jwk := map[string]interface{}{
225 "kty": "EC",
226 "crv": "P-256",
227 "y": "test-y",
228 }
229
230 _, err := CalculateJWKThumbprint(jwk)
231 if err == nil {
232 t.Error("Expected error for missing x, got nil")
233 }
234 if err != nil && !contains(err.Error(), "missing x") {
235 t.Errorf("Expected error about missing x, got: %v", err)
236 }
237}
238
239func TestCalculateJWKThumbprint_EC_MissingY(t *testing.T) {
240 jwk := map[string]interface{}{
241 "kty": "EC",
242 "crv": "P-256",
243 "x": "test-x",
244 }
245
246 _, err := CalculateJWKThumbprint(jwk)
247 if err == nil {
248 t.Error("Expected error for missing y, got nil")
249 }
250 if err != nil && !contains(err.Error(), "missing y") {
251 t.Errorf("Expected error about missing y, got: %v", err)
252 }
253}
254
255func TestCalculateJWKThumbprint_RSA(t *testing.T) {
256 // Test RSA key thumbprint calculation
257 jwk := map[string]interface{}{
258 "kty": "RSA",
259 "e": "AQAB",
260 "n": "test-modulus",
261 }
262
263 thumbprint, err := CalculateJWKThumbprint(jwk)
264 if err != nil {
265 t.Fatalf("CalculateJWKThumbprint failed for RSA: %v", err)
266 }
267
268 if thumbprint == "" {
269 t.Error("Expected non-empty thumbprint for RSA key")
270 }
271}
272
273func TestCalculateJWKThumbprint_OKP(t *testing.T) {
274 // Test OKP (Octet Key Pair) thumbprint calculation
275 jwk := map[string]interface{}{
276 "kty": "OKP",
277 "crv": "Ed25519",
278 "x": "test-x-coordinate",
279 }
280
281 thumbprint, err := CalculateJWKThumbprint(jwk)
282 if err != nil {
283 t.Fatalf("CalculateJWKThumbprint failed for OKP: %v", err)
284 }
285
286 if thumbprint == "" {
287 t.Error("Expected non-empty thumbprint for OKP key")
288 }
289}
290
291func TestCalculateJWKThumbprint_UnsupportedKeyType(t *testing.T) {
292 jwk := map[string]interface{}{
293 "kty": "UNKNOWN",
294 }
295
296 _, err := CalculateJWKThumbprint(jwk)
297 if err == nil {
298 t.Error("Expected error for unsupported key type, got nil")
299 }
300 if err != nil && !contains(err.Error(), "unsupported JWK key type") {
301 t.Errorf("Expected error about unsupported key type, got: %v", err)
302 }
303}
304
305func TestCalculateJWKThumbprint_CanonicalJSON(t *testing.T) {
306 // RFC 7638 requires lexicographic ordering of keys in canonical JSON
307 // This test verifies that the canonical JSON is correctly ordered
308
309 jwk := map[string]interface{}{
310 "kty": "EC",
311 "crv": "P-256",
312 "x": "x-coord",
313 "y": "y-coord",
314 }
315
316 // The canonical JSON should be: {"crv":"P-256","kty":"EC","x":"x-coord","y":"y-coord"}
317 // (lexicographically ordered: crv, kty, x, y)
318
319 canonical := map[string]string{
320 "crv": "P-256",
321 "kty": "EC",
322 "x": "x-coord",
323 "y": "y-coord",
324 }
325
326 canonicalJSON, err := json.Marshal(canonical)
327 if err != nil {
328 t.Fatalf("Failed to marshal canonical JSON: %v", err)
329 }
330
331 expectedHash := sha256.Sum256(canonicalJSON)
332 expectedThumbprint := base64.RawURLEncoding.EncodeToString(expectedHash[:])
333
334 actualThumbprint, err := CalculateJWKThumbprint(jwk)
335 if err != nil {
336 t.Fatalf("CalculateJWKThumbprint failed: %v", err)
337 }
338
339 if actualThumbprint != expectedThumbprint {
340 t.Errorf("Thumbprint doesn't match expected canonical JSON hash\nExpected: %s\nGot: %s",
341 expectedThumbprint, actualThumbprint)
342 }
343}
344
345// === DPoP Proof Verification Tests ===
346
347func TestVerifyDPoPProof_Valid(t *testing.T) {
348 verifier := NewDPoPVerifier()
349 key := generateTestES256Key(t)
350
351 method := "POST"
352 uri := "https://api.example.com/resource"
353 iat := time.Now()
354 jti := uuid.New().String()
355
356 proof := createDPoPProof(t, key, method, uri, iat, jti)
357
358 result, err := verifier.VerifyDPoPProof(proof, method, uri)
359 if err != nil {
360 t.Fatalf("VerifyDPoPProof failed for valid proof: %v", err)
361 }
362
363 if result == nil {
364 t.Fatal("Expected non-nil proof result")
365 }
366
367 if result.Claims.HTTPMethod != method {
368 t.Errorf("Expected method %s, got %s", method, result.Claims.HTTPMethod)
369 }
370
371 if result.Claims.HTTPURI != uri {
372 t.Errorf("Expected URI %s, got %s", uri, result.Claims.HTTPURI)
373 }
374
375 if result.Claims.ID != jti {
376 t.Errorf("Expected jti %s, got %s", jti, result.Claims.ID)
377 }
378
379 if result.Thumbprint != key.thumbprint {
380 t.Errorf("Expected thumbprint %s, got %s", key.thumbprint, result.Thumbprint)
381 }
382}
383
384func TestVerifyDPoPProof_InvalidSignature(t *testing.T) {
385 verifier := NewDPoPVerifier()
386 key := generateTestES256Key(t)
387 wrongKey := generateTestES256Key(t)
388
389 method := "POST"
390 uri := "https://api.example.com/resource"
391 iat := time.Now()
392 jti := uuid.New().String()
393
394 // Create proof with one key
395 proof := createDPoPProof(t, key, method, uri, iat, jti)
396
397 // Parse and modify to use wrong key's JWK in header (signature won't match)
398 parts := splitJWT(proof)
399 header := parseJWTHeader(t, parts[0])
400 header["jwk"] = wrongKey.jwk
401 modifiedHeader := encodeJSON(t, header)
402 tamperedProof := modifiedHeader + "." + parts[1] + "." + parts[2]
403
404 _, err := verifier.VerifyDPoPProof(tamperedProof, method, uri)
405 if err == nil {
406 t.Error("Expected error for invalid signature, got nil")
407 }
408 if err != nil && !contains(err.Error(), "signature verification failed") {
409 t.Errorf("Expected signature verification error, got: %v", err)
410 }
411}
412
413func TestVerifyDPoPProof_WrongHTTPMethod(t *testing.T) {
414 verifier := NewDPoPVerifier()
415 key := generateTestES256Key(t)
416
417 method := "POST"
418 wrongMethod := "GET"
419 uri := "https://api.example.com/resource"
420 iat := time.Now()
421 jti := uuid.New().String()
422
423 proof := createDPoPProof(t, key, method, uri, iat, jti)
424
425 _, err := verifier.VerifyDPoPProof(proof, wrongMethod, uri)
426 if err == nil {
427 t.Error("Expected error for HTTP method mismatch, got nil")
428 }
429 if err != nil && !contains(err.Error(), "htm mismatch") {
430 t.Errorf("Expected htm mismatch error, got: %v", err)
431 }
432}
433
434func TestVerifyDPoPProof_WrongURI(t *testing.T) {
435 verifier := NewDPoPVerifier()
436 key := generateTestES256Key(t)
437
438 method := "POST"
439 uri := "https://api.example.com/resource"
440 wrongURI := "https://api.example.com/different"
441 iat := time.Now()
442 jti := uuid.New().String()
443
444 proof := createDPoPProof(t, key, method, uri, iat, jti)
445
446 _, err := verifier.VerifyDPoPProof(proof, method, wrongURI)
447 if err == nil {
448 t.Error("Expected error for URI mismatch, got nil")
449 }
450 if err != nil && !contains(err.Error(), "htu mismatch") {
451 t.Errorf("Expected htu mismatch error, got: %v", err)
452 }
453}
454
455func TestVerifyDPoPProof_URIWithQuery(t *testing.T) {
456 // URI comparison should strip query and fragment
457 verifier := NewDPoPVerifier()
458 key := generateTestES256Key(t)
459
460 method := "POST"
461 baseURI := "https://api.example.com/resource"
462 uriWithQuery := baseURI + "?param=value"
463 iat := time.Now()
464 jti := uuid.New().String()
465
466 proof := createDPoPProof(t, key, method, baseURI, iat, jti)
467
468 // Should succeed because query is stripped
469 _, err := verifier.VerifyDPoPProof(proof, method, uriWithQuery)
470 if err != nil {
471 t.Fatalf("VerifyDPoPProof failed for URI with query: %v", err)
472 }
473}
474
475func TestVerifyDPoPProof_URIWithFragment(t *testing.T) {
476 // URI comparison should strip query and fragment
477 verifier := NewDPoPVerifier()
478 key := generateTestES256Key(t)
479
480 method := "POST"
481 baseURI := "https://api.example.com/resource"
482 uriWithFragment := baseURI + "#section"
483 iat := time.Now()
484 jti := uuid.New().String()
485
486 proof := createDPoPProof(t, key, method, baseURI, iat, jti)
487
488 // Should succeed because fragment is stripped
489 _, err := verifier.VerifyDPoPProof(proof, method, uriWithFragment)
490 if err != nil {
491 t.Fatalf("VerifyDPoPProof failed for URI with fragment: %v", err)
492 }
493}
494
495func TestVerifyDPoPProof_ExpiredProof(t *testing.T) {
496 verifier := NewDPoPVerifier()
497 key := generateTestES256Key(t)
498
499 method := "POST"
500 uri := "https://api.example.com/resource"
501 // Proof issued 10 minutes ago (exceeds default MaxProofAge of 5 minutes)
502 iat := time.Now().Add(-10 * time.Minute)
503 jti := uuid.New().String()
504
505 proof := createDPoPProof(t, key, method, uri, iat, jti)
506
507 _, err := verifier.VerifyDPoPProof(proof, method, uri)
508 if err == nil {
509 t.Error("Expected error for expired proof, got nil")
510 }
511 if err != nil && !contains(err.Error(), "too old") {
512 t.Errorf("Expected 'too old' error, got: %v", err)
513 }
514}
515
516func TestVerifyDPoPProof_FutureProof(t *testing.T) {
517 verifier := NewDPoPVerifier()
518 key := generateTestES256Key(t)
519
520 method := "POST"
521 uri := "https://api.example.com/resource"
522 // Proof issued 1 minute in the future (exceeds MaxClockSkew)
523 iat := time.Now().Add(1 * time.Minute)
524 jti := uuid.New().String()
525
526 proof := createDPoPProof(t, key, method, uri, iat, jti)
527
528 _, err := verifier.VerifyDPoPProof(proof, method, uri)
529 if err == nil {
530 t.Error("Expected error for future proof, got nil")
531 }
532 if err != nil && !contains(err.Error(), "in the future") {
533 t.Errorf("Expected 'in the future' error, got: %v", err)
534 }
535}
536
537func TestVerifyDPoPProof_WithinClockSkew(t *testing.T) {
538 verifier := NewDPoPVerifier()
539 key := generateTestES256Key(t)
540
541 method := "POST"
542 uri := "https://api.example.com/resource"
543 // Proof issued 15 seconds in the future (within MaxClockSkew of 30s)
544 iat := time.Now().Add(15 * time.Second)
545 jti := uuid.New().String()
546
547 proof := createDPoPProof(t, key, method, uri, iat, jti)
548
549 _, err := verifier.VerifyDPoPProof(proof, method, uri)
550 if err != nil {
551 t.Fatalf("VerifyDPoPProof failed for proof within clock skew: %v", err)
552 }
553}
554
555func TestVerifyDPoPProof_MissingJti(t *testing.T) {
556 verifier := NewDPoPVerifier()
557 key := generateTestES256Key(t)
558
559 method := "POST"
560 uri := "https://api.example.com/resource"
561 iat := time.Now()
562
563 claims := &DPoPClaims{
564 RegisteredClaims: jwt.RegisteredClaims{
565 // No ID (jti)
566 IssuedAt: jwt.NewNumericDate(iat),
567 },
568 HTTPMethod: method,
569 HTTPURI: uri,
570 }
571
572 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
573 token.Header["typ"] = "dpop+jwt"
574 token.Header["jwk"] = key.jwk
575
576 proof, err := token.SignedString(key.privateKey)
577 if err != nil {
578 t.Fatalf("Failed to create test proof: %v", err)
579 }
580
581 _, err = verifier.VerifyDPoPProof(proof, method, uri)
582 if err == nil {
583 t.Error("Expected error for missing jti, got nil")
584 }
585 if err != nil && !contains(err.Error(), "missing jti") {
586 t.Errorf("Expected missing jti error, got: %v", err)
587 }
588}
589
590func TestVerifyDPoPProof_MissingTypHeader(t *testing.T) {
591 verifier := NewDPoPVerifier()
592 key := generateTestES256Key(t)
593
594 method := "POST"
595 uri := "https://api.example.com/resource"
596 iat := time.Now()
597 jti := uuid.New().String()
598
599 claims := &DPoPClaims{
600 RegisteredClaims: jwt.RegisteredClaims{
601 ID: jti,
602 IssuedAt: jwt.NewNumericDate(iat),
603 },
604 HTTPMethod: method,
605 HTTPURI: uri,
606 }
607
608 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
609 // Don't set typ header
610 token.Header["jwk"] = key.jwk
611
612 proof, err := token.SignedString(key.privateKey)
613 if err != nil {
614 t.Fatalf("Failed to create test proof: %v", err)
615 }
616
617 _, err = verifier.VerifyDPoPProof(proof, method, uri)
618 if err == nil {
619 t.Error("Expected error for missing typ header, got nil")
620 }
621 if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
622 t.Errorf("Expected typ header error, got: %v", err)
623 }
624}
625
626func TestVerifyDPoPProof_WrongTypHeader(t *testing.T) {
627 verifier := NewDPoPVerifier()
628 key := generateTestES256Key(t)
629
630 method := "POST"
631 uri := "https://api.example.com/resource"
632 iat := time.Now()
633 jti := uuid.New().String()
634
635 claims := &DPoPClaims{
636 RegisteredClaims: jwt.RegisteredClaims{
637 ID: jti,
638 IssuedAt: jwt.NewNumericDate(iat),
639 },
640 HTTPMethod: method,
641 HTTPURI: uri,
642 }
643
644 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
645 token.Header["typ"] = "JWT" // Wrong typ
646 token.Header["jwk"] = key.jwk
647
648 proof, err := token.SignedString(key.privateKey)
649 if err != nil {
650 t.Fatalf("Failed to create test proof: %v", err)
651 }
652
653 _, err = verifier.VerifyDPoPProof(proof, method, uri)
654 if err == nil {
655 t.Error("Expected error for wrong typ header, got nil")
656 }
657 if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
658 t.Errorf("Expected typ header error, got: %v", err)
659 }
660}
661
662func TestVerifyDPoPProof_MissingJWK(t *testing.T) {
663 verifier := NewDPoPVerifier()
664 key := generateTestES256Key(t)
665
666 method := "POST"
667 uri := "https://api.example.com/resource"
668 iat := time.Now()
669 jti := uuid.New().String()
670
671 claims := &DPoPClaims{
672 RegisteredClaims: jwt.RegisteredClaims{
673 ID: jti,
674 IssuedAt: jwt.NewNumericDate(iat),
675 },
676 HTTPMethod: method,
677 HTTPURI: uri,
678 }
679
680 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
681 token.Header["typ"] = "dpop+jwt"
682 // Don't include JWK
683
684 proof, err := token.SignedString(key.privateKey)
685 if err != nil {
686 t.Fatalf("Failed to create test proof: %v", err)
687 }
688
689 _, err = verifier.VerifyDPoPProof(proof, method, uri)
690 if err == nil {
691 t.Error("Expected error for missing jwk header, got nil")
692 }
693 if err != nil && !contains(err.Error(), "missing jwk") {
694 t.Errorf("Expected missing jwk error, got: %v", err)
695 }
696}
697
698func TestVerifyDPoPProof_CustomTimeSettings(t *testing.T) {
699 verifier := &DPoPVerifier{
700 MaxClockSkew: 1 * time.Minute,
701 MaxProofAge: 10 * time.Minute,
702 }
703 key := generateTestES256Key(t)
704
705 method := "POST"
706 uri := "https://api.example.com/resource"
707 // Proof issued 50 seconds in the future (within custom MaxClockSkew)
708 iat := time.Now().Add(50 * time.Second)
709 jti := uuid.New().String()
710
711 proof := createDPoPProof(t, key, method, uri, iat, jti)
712
713 _, err := verifier.VerifyDPoPProof(proof, method, uri)
714 if err != nil {
715 t.Fatalf("VerifyDPoPProof failed with custom time settings: %v", err)
716 }
717}
718
719func TestVerifyDPoPProof_HTTPMethodCaseInsensitive(t *testing.T) {
720 // HTTP method comparison should be case-insensitive per spec
721 verifier := NewDPoPVerifier()
722 key := generateTestES256Key(t)
723
724 method := "post"
725 uri := "https://api.example.com/resource"
726 iat := time.Now()
727 jti := uuid.New().String()
728
729 proof := createDPoPProof(t, key, method, uri, iat, jti)
730
731 // Verify with uppercase method
732 _, err := verifier.VerifyDPoPProof(proof, "POST", uri)
733 if err != nil {
734 t.Fatalf("VerifyDPoPProof failed for case-insensitive method: %v", err)
735 }
736}
737
738// === Token Binding Verification Tests ===
739
740func TestVerifyTokenBinding_Matching(t *testing.T) {
741 verifier := NewDPoPVerifier()
742 key := generateTestES256Key(t)
743
744 method := "POST"
745 uri := "https://api.example.com/resource"
746 iat := time.Now()
747 jti := uuid.New().String()
748
749 proof := createDPoPProof(t, key, method, uri, iat, jti)
750
751 result, err := verifier.VerifyDPoPProof(proof, method, uri)
752 if err != nil {
753 t.Fatalf("VerifyDPoPProof failed: %v", err)
754 }
755
756 // Verify token binding with matching thumbprint
757 err = verifier.VerifyTokenBinding(result, key.thumbprint)
758 if err != nil {
759 t.Fatalf("VerifyTokenBinding failed for matching thumbprint: %v", err)
760 }
761}
762
763func TestVerifyTokenBinding_Mismatch(t *testing.T) {
764 verifier := NewDPoPVerifier()
765 key := generateTestES256Key(t)
766 wrongKey := generateTestES256Key(t)
767
768 method := "POST"
769 uri := "https://api.example.com/resource"
770 iat := time.Now()
771 jti := uuid.New().String()
772
773 proof := createDPoPProof(t, key, method, uri, iat, jti)
774
775 result, err := verifier.VerifyDPoPProof(proof, method, uri)
776 if err != nil {
777 t.Fatalf("VerifyDPoPProof failed: %v", err)
778 }
779
780 // Verify token binding with wrong thumbprint
781 err = verifier.VerifyTokenBinding(result, wrongKey.thumbprint)
782 if err == nil {
783 t.Error("Expected error for thumbprint mismatch, got nil")
784 }
785 if err != nil && !contains(err.Error(), "thumbprint mismatch") {
786 t.Errorf("Expected thumbprint mismatch error, got: %v", err)
787 }
788}
789
790// === ExtractCnfJkt Tests ===
791
792func TestExtractCnfJkt_Valid(t *testing.T) {
793 expectedJkt := "test-thumbprint-123"
794 claims := &Claims{
795 Confirmation: map[string]interface{}{
796 "jkt": expectedJkt,
797 },
798 }
799
800 jkt, err := ExtractCnfJkt(claims)
801 if err != nil {
802 t.Fatalf("ExtractCnfJkt failed for valid claims: %v", err)
803 }
804
805 if jkt != expectedJkt {
806 t.Errorf("Expected jkt %s, got %s", expectedJkt, jkt)
807 }
808}
809
810func TestExtractCnfJkt_MissingCnf(t *testing.T) {
811 claims := &Claims{
812 // No Confirmation
813 }
814
815 _, err := ExtractCnfJkt(claims)
816 if err == nil {
817 t.Error("Expected error for missing cnf, got nil")
818 }
819 if err != nil && !contains(err.Error(), "missing cnf claim") {
820 t.Errorf("Expected missing cnf error, got: %v", err)
821 }
822}
823
824func TestExtractCnfJkt_NilCnf(t *testing.T) {
825 claims := &Claims{
826 Confirmation: nil,
827 }
828
829 _, err := ExtractCnfJkt(claims)
830 if err == nil {
831 t.Error("Expected error for nil cnf, got nil")
832 }
833 if err != nil && !contains(err.Error(), "missing cnf claim") {
834 t.Errorf("Expected missing cnf error, got: %v", err)
835 }
836}
837
838func TestExtractCnfJkt_MissingJkt(t *testing.T) {
839 claims := &Claims{
840 Confirmation: map[string]interface{}{
841 "other": "value",
842 },
843 }
844
845 _, err := ExtractCnfJkt(claims)
846 if err == nil {
847 t.Error("Expected error for missing jkt, got nil")
848 }
849 if err != nil && !contains(err.Error(), "missing jkt") {
850 t.Errorf("Expected missing jkt error, got: %v", err)
851 }
852}
853
854func TestExtractCnfJkt_EmptyJkt(t *testing.T) {
855 claims := &Claims{
856 Confirmation: map[string]interface{}{
857 "jkt": "",
858 },
859 }
860
861 _, err := ExtractCnfJkt(claims)
862 if err == nil {
863 t.Error("Expected error for empty jkt, got nil")
864 }
865 if err != nil && !contains(err.Error(), "missing jkt") {
866 t.Errorf("Expected missing jkt error, got: %v", err)
867 }
868}
869
870func TestExtractCnfJkt_WrongType(t *testing.T) {
871 claims := &Claims{
872 Confirmation: map[string]interface{}{
873 "jkt": 123, // Not a string
874 },
875 }
876
877 _, err := ExtractCnfJkt(claims)
878 if err == nil {
879 t.Error("Expected error for wrong type jkt, got nil")
880 }
881 if err != nil && !contains(err.Error(), "missing jkt") {
882 t.Errorf("Expected missing jkt error, got: %v", err)
883 }
884}
885
886// === Helper Functions for Tests ===
887
888// splitJWT splits a JWT into its three parts
889func splitJWT(token string) []string {
890 return []string{
891 token[:strings.IndexByte(token, '.')],
892 token[strings.IndexByte(token, '.')+1 : strings.LastIndexByte(token, '.')],
893 token[strings.LastIndexByte(token, '.')+1:],
894 }
895}
896
897// parseJWTHeader parses a base64url-encoded JWT header
898func parseJWTHeader(t *testing.T, encoded string) map[string]interface{} {
899 t.Helper()
900 decoded, err := base64.RawURLEncoding.DecodeString(encoded)
901 if err != nil {
902 t.Fatalf("Failed to decode header: %v", err)
903 }
904
905 var header map[string]interface{}
906 if err := json.Unmarshal(decoded, &header); err != nil {
907 t.Fatalf("Failed to unmarshal header: %v", err)
908 }
909
910 return header
911}
912
913// encodeJSON encodes a value to base64url-encoded JSON
914func encodeJSON(t *testing.T, v interface{}) string {
915 t.Helper()
916 data, err := json.Marshal(v)
917 if err != nil {
918 t.Fatalf("Failed to marshal JSON: %v", err)
919 }
920 return base64.RawURLEncoding.EncodeToString(data)
921}