···
14
+
"github.com/golang-jwt/jwt/v5"
15
+
"github.com/google/uuid"
18
+
// === Test Helpers ===
20
+
// testECKey holds a test ES256 key pair
21
+
type testECKey struct {
22
+
privateKey *ecdsa.PrivateKey
23
+
publicKey *ecdsa.PublicKey
24
+
jwk map[string]interface{}
28
+
// generateTestES256Key generates a test ES256 key pair and JWK
29
+
func generateTestES256Key(t *testing.T) *testECKey {
32
+
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
34
+
t.Fatalf("Failed to generate test key: %v", err)
37
+
// Encode public key coordinates as base64url
38
+
xBytes := privateKey.PublicKey.X.Bytes()
39
+
yBytes := privateKey.PublicKey.Y.Bytes()
41
+
// P-256 coordinates must be 32 bytes (pad if needed)
42
+
xBytes = padTo32Bytes(xBytes)
43
+
yBytes = padTo32Bytes(yBytes)
45
+
x := base64.RawURLEncoding.EncodeToString(xBytes)
46
+
y := base64.RawURLEncoding.EncodeToString(yBytes)
48
+
jwk := map[string]interface{}{
55
+
// Calculate thumbprint
56
+
thumbprint, err := CalculateJWKThumbprint(jwk)
58
+
t.Fatalf("Failed to calculate thumbprint: %v", err)
62
+
privateKey: privateKey,
63
+
publicKey: &privateKey.PublicKey,
65
+
thumbprint: thumbprint,
69
+
// padTo32Bytes pads a byte slice to 32 bytes (required for P-256 coordinates)
70
+
func padTo32Bytes(b []byte) []byte {
74
+
padded := make([]byte, 32)
75
+
copy(padded[32-len(b):], b)
79
+
// createDPoPProof creates a DPoP proof JWT for testing
80
+
func createDPoPProof(t *testing.T, key *testECKey, method, uri string, iat time.Time, jti string) string {
83
+
claims := &DPoPClaims{
84
+
RegisteredClaims: jwt.RegisteredClaims{
86
+
IssuedAt: jwt.NewNumericDate(iat),
92
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
93
+
token.Header["typ"] = "dpop+jwt"
94
+
token.Header["jwk"] = key.jwk
96
+
tokenString, err := token.SignedString(key.privateKey)
98
+
t.Fatalf("Failed to create DPoP proof: %v", err)
104
+
// === JWK Thumbprint Tests (RFC 7638) ===
106
+
func 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{}{
111
+
"x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
112
+
"y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE",
115
+
thumbprint, err := CalculateJWKThumbprint(jwk)
117
+
t.Fatalf("CalculateJWKThumbprint failed: %v", err)
120
+
if thumbprint == "" {
121
+
t.Error("Expected non-empty thumbprint")
124
+
// Verify it's valid base64url
125
+
_, err = base64.RawURLEncoding.DecodeString(thumbprint)
127
+
t.Errorf("Thumbprint is not valid base64url: %v", err)
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))
136
+
func TestCalculateJWKThumbprint_Deterministic(t *testing.T) {
137
+
// Same key should produce same thumbprint
138
+
jwk := map[string]interface{}{
141
+
"x": "test-x-coordinate",
142
+
"y": "test-y-coordinate",
145
+
thumbprint1, err := CalculateJWKThumbprint(jwk)
147
+
t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
150
+
thumbprint2, err := CalculateJWKThumbprint(jwk)
152
+
t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
155
+
if thumbprint1 != thumbprint2 {
156
+
t.Errorf("Thumbprints are not deterministic: %s != %s", thumbprint1, thumbprint2)
160
+
func TestCalculateJWKThumbprint_DifferentKeys(t *testing.T) {
161
+
// Different keys should produce different thumbprints
162
+
jwk1 := map[string]interface{}{
165
+
"x": "coordinate-x-1",
166
+
"y": "coordinate-y-1",
169
+
jwk2 := map[string]interface{}{
172
+
"x": "coordinate-x-2",
173
+
"y": "coordinate-y-2",
176
+
thumbprint1, err := CalculateJWKThumbprint(jwk1)
178
+
t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
181
+
thumbprint2, err := CalculateJWKThumbprint(jwk2)
183
+
t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
186
+
if thumbprint1 == thumbprint2 {
187
+
t.Error("Different keys produced same thumbprint (collision)")
191
+
func TestCalculateJWKThumbprint_MissingKty(t *testing.T) {
192
+
jwk := map[string]interface{}{
198
+
_, err := CalculateJWKThumbprint(jwk)
200
+
t.Error("Expected error for missing kty, got nil")
202
+
if err != nil && !contains(err.Error(), "missing kty") {
203
+
t.Errorf("Expected error about missing kty, got: %v", err)
207
+
func TestCalculateJWKThumbprint_EC_MissingCrv(t *testing.T) {
208
+
jwk := map[string]interface{}{
214
+
_, err := CalculateJWKThumbprint(jwk)
216
+
t.Error("Expected error for missing crv, got nil")
218
+
if err != nil && !contains(err.Error(), "missing crv") {
219
+
t.Errorf("Expected error about missing crv, got: %v", err)
223
+
func TestCalculateJWKThumbprint_EC_MissingX(t *testing.T) {
224
+
jwk := map[string]interface{}{
230
+
_, err := CalculateJWKThumbprint(jwk)
232
+
t.Error("Expected error for missing x, got nil")
234
+
if err != nil && !contains(err.Error(), "missing x") {
235
+
t.Errorf("Expected error about missing x, got: %v", err)
239
+
func TestCalculateJWKThumbprint_EC_MissingY(t *testing.T) {
240
+
jwk := map[string]interface{}{
246
+
_, err := CalculateJWKThumbprint(jwk)
248
+
t.Error("Expected error for missing y, got nil")
250
+
if err != nil && !contains(err.Error(), "missing y") {
251
+
t.Errorf("Expected error about missing y, got: %v", err)
255
+
func TestCalculateJWKThumbprint_RSA(t *testing.T) {
256
+
// Test RSA key thumbprint calculation
257
+
jwk := map[string]interface{}{
260
+
"n": "test-modulus",
263
+
thumbprint, err := CalculateJWKThumbprint(jwk)
265
+
t.Fatalf("CalculateJWKThumbprint failed for RSA: %v", err)
268
+
if thumbprint == "" {
269
+
t.Error("Expected non-empty thumbprint for RSA key")
273
+
func TestCalculateJWKThumbprint_OKP(t *testing.T) {
274
+
// Test OKP (Octet Key Pair) thumbprint calculation
275
+
jwk := map[string]interface{}{
278
+
"x": "test-x-coordinate",
281
+
thumbprint, err := CalculateJWKThumbprint(jwk)
283
+
t.Fatalf("CalculateJWKThumbprint failed for OKP: %v", err)
286
+
if thumbprint == "" {
287
+
t.Error("Expected non-empty thumbprint for OKP key")
291
+
func TestCalculateJWKThumbprint_UnsupportedKeyType(t *testing.T) {
292
+
jwk := map[string]interface{}{
296
+
_, err := CalculateJWKThumbprint(jwk)
298
+
t.Error("Expected error for unsupported key type, got nil")
300
+
if err != nil && !contains(err.Error(), "unsupported JWK key type") {
301
+
t.Errorf("Expected error about unsupported key type, got: %v", err)
305
+
func 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
309
+
jwk := map[string]interface{}{
316
+
// The canonical JSON should be: {"crv":"P-256","kty":"EC","x":"x-coord","y":"y-coord"}
317
+
// (lexicographically ordered: crv, kty, x, y)
319
+
canonical := map[string]string{
326
+
canonicalJSON, err := json.Marshal(canonical)
328
+
t.Fatalf("Failed to marshal canonical JSON: %v", err)
331
+
expectedHash := sha256.Sum256(canonicalJSON)
332
+
expectedThumbprint := base64.RawURLEncoding.EncodeToString(expectedHash[:])
334
+
actualThumbprint, err := CalculateJWKThumbprint(jwk)
336
+
t.Fatalf("CalculateJWKThumbprint failed: %v", err)
339
+
if actualThumbprint != expectedThumbprint {
340
+
t.Errorf("Thumbprint doesn't match expected canonical JSON hash\nExpected: %s\nGot: %s",
341
+
expectedThumbprint, actualThumbprint)
345
+
// === DPoP Proof Verification Tests ===
347
+
func TestVerifyDPoPProof_Valid(t *testing.T) {
348
+
verifier := NewDPoPVerifier()
349
+
key := generateTestES256Key(t)
352
+
uri := "https://api.example.com/resource"
354
+
jti := uuid.New().String()
356
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
358
+
result, err := verifier.VerifyDPoPProof(proof, method, uri)
360
+
t.Fatalf("VerifyDPoPProof failed for valid proof: %v", err)
364
+
t.Fatal("Expected non-nil proof result")
367
+
if result.Claims.HTTPMethod != method {
368
+
t.Errorf("Expected method %s, got %s", method, result.Claims.HTTPMethod)
371
+
if result.Claims.HTTPURI != uri {
372
+
t.Errorf("Expected URI %s, got %s", uri, result.Claims.HTTPURI)
375
+
if result.Claims.ID != jti {
376
+
t.Errorf("Expected jti %s, got %s", jti, result.Claims.ID)
379
+
if result.Thumbprint != key.thumbprint {
380
+
t.Errorf("Expected thumbprint %s, got %s", key.thumbprint, result.Thumbprint)
384
+
func TestVerifyDPoPProof_InvalidSignature(t *testing.T) {
385
+
verifier := NewDPoPVerifier()
386
+
key := generateTestES256Key(t)
387
+
wrongKey := generateTestES256Key(t)
390
+
uri := "https://api.example.com/resource"
392
+
jti := uuid.New().String()
394
+
// Create proof with one key
395
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
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]
404
+
_, err := verifier.VerifyDPoPProof(tamperedProof, method, uri)
406
+
t.Error("Expected error for invalid signature, got nil")
408
+
if err != nil && !contains(err.Error(), "signature verification failed") {
409
+
t.Errorf("Expected signature verification error, got: %v", err)
413
+
func TestVerifyDPoPProof_WrongHTTPMethod(t *testing.T) {
414
+
verifier := NewDPoPVerifier()
415
+
key := generateTestES256Key(t)
418
+
wrongMethod := "GET"
419
+
uri := "https://api.example.com/resource"
421
+
jti := uuid.New().String()
423
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
425
+
_, err := verifier.VerifyDPoPProof(proof, wrongMethod, uri)
427
+
t.Error("Expected error for HTTP method mismatch, got nil")
429
+
if err != nil && !contains(err.Error(), "htm mismatch") {
430
+
t.Errorf("Expected htm mismatch error, got: %v", err)
434
+
func TestVerifyDPoPProof_WrongURI(t *testing.T) {
435
+
verifier := NewDPoPVerifier()
436
+
key := generateTestES256Key(t)
439
+
uri := "https://api.example.com/resource"
440
+
wrongURI := "https://api.example.com/different"
442
+
jti := uuid.New().String()
444
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
446
+
_, err := verifier.VerifyDPoPProof(proof, method, wrongURI)
448
+
t.Error("Expected error for URI mismatch, got nil")
450
+
if err != nil && !contains(err.Error(), "htu mismatch") {
451
+
t.Errorf("Expected htu mismatch error, got: %v", err)
455
+
func TestVerifyDPoPProof_URIWithQuery(t *testing.T) {
456
+
// URI comparison should strip query and fragment
457
+
verifier := NewDPoPVerifier()
458
+
key := generateTestES256Key(t)
461
+
baseURI := "https://api.example.com/resource"
462
+
uriWithQuery := baseURI + "?param=value"
464
+
jti := uuid.New().String()
466
+
proof := createDPoPProof(t, key, method, baseURI, iat, jti)
468
+
// Should succeed because query is stripped
469
+
_, err := verifier.VerifyDPoPProof(proof, method, uriWithQuery)
471
+
t.Fatalf("VerifyDPoPProof failed for URI with query: %v", err)
475
+
func TestVerifyDPoPProof_URIWithFragment(t *testing.T) {
476
+
// URI comparison should strip query and fragment
477
+
verifier := NewDPoPVerifier()
478
+
key := generateTestES256Key(t)
481
+
baseURI := "https://api.example.com/resource"
482
+
uriWithFragment := baseURI + "#section"
484
+
jti := uuid.New().String()
486
+
proof := createDPoPProof(t, key, method, baseURI, iat, jti)
488
+
// Should succeed because fragment is stripped
489
+
_, err := verifier.VerifyDPoPProof(proof, method, uriWithFragment)
491
+
t.Fatalf("VerifyDPoPProof failed for URI with fragment: %v", err)
495
+
func TestVerifyDPoPProof_ExpiredProof(t *testing.T) {
496
+
verifier := NewDPoPVerifier()
497
+
key := generateTestES256Key(t)
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()
505
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
507
+
_, err := verifier.VerifyDPoPProof(proof, method, uri)
509
+
t.Error("Expected error for expired proof, got nil")
511
+
if err != nil && !contains(err.Error(), "too old") {
512
+
t.Errorf("Expected 'too old' error, got: %v", err)
516
+
func TestVerifyDPoPProof_FutureProof(t *testing.T) {
517
+
verifier := NewDPoPVerifier()
518
+
key := generateTestES256Key(t)
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()
526
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
528
+
_, err := verifier.VerifyDPoPProof(proof, method, uri)
530
+
t.Error("Expected error for future proof, got nil")
532
+
if err != nil && !contains(err.Error(), "in the future") {
533
+
t.Errorf("Expected 'in the future' error, got: %v", err)
537
+
func TestVerifyDPoPProof_WithinClockSkew(t *testing.T) {
538
+
verifier := NewDPoPVerifier()
539
+
key := generateTestES256Key(t)
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()
547
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
549
+
_, err := verifier.VerifyDPoPProof(proof, method, uri)
551
+
t.Fatalf("VerifyDPoPProof failed for proof within clock skew: %v", err)
555
+
func TestVerifyDPoPProof_MissingJti(t *testing.T) {
556
+
verifier := NewDPoPVerifier()
557
+
key := generateTestES256Key(t)
560
+
uri := "https://api.example.com/resource"
563
+
claims := &DPoPClaims{
564
+
RegisteredClaims: jwt.RegisteredClaims{
566
+
IssuedAt: jwt.NewNumericDate(iat),
568
+
HTTPMethod: method,
572
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
573
+
token.Header["typ"] = "dpop+jwt"
574
+
token.Header["jwk"] = key.jwk
576
+
proof, err := token.SignedString(key.privateKey)
578
+
t.Fatalf("Failed to create test proof: %v", err)
581
+
_, err = verifier.VerifyDPoPProof(proof, method, uri)
583
+
t.Error("Expected error for missing jti, got nil")
585
+
if err != nil && !contains(err.Error(), "missing jti") {
586
+
t.Errorf("Expected missing jti error, got: %v", err)
590
+
func TestVerifyDPoPProof_MissingTypHeader(t *testing.T) {
591
+
verifier := NewDPoPVerifier()
592
+
key := generateTestES256Key(t)
595
+
uri := "https://api.example.com/resource"
597
+
jti := uuid.New().String()
599
+
claims := &DPoPClaims{
600
+
RegisteredClaims: jwt.RegisteredClaims{
602
+
IssuedAt: jwt.NewNumericDate(iat),
604
+
HTTPMethod: method,
608
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
609
+
// Don't set typ header
610
+
token.Header["jwk"] = key.jwk
612
+
proof, err := token.SignedString(key.privateKey)
614
+
t.Fatalf("Failed to create test proof: %v", err)
617
+
_, err = verifier.VerifyDPoPProof(proof, method, uri)
619
+
t.Error("Expected error for missing typ header, got nil")
621
+
if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
622
+
t.Errorf("Expected typ header error, got: %v", err)
626
+
func TestVerifyDPoPProof_WrongTypHeader(t *testing.T) {
627
+
verifier := NewDPoPVerifier()
628
+
key := generateTestES256Key(t)
631
+
uri := "https://api.example.com/resource"
633
+
jti := uuid.New().String()
635
+
claims := &DPoPClaims{
636
+
RegisteredClaims: jwt.RegisteredClaims{
638
+
IssuedAt: jwt.NewNumericDate(iat),
640
+
HTTPMethod: method,
644
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
645
+
token.Header["typ"] = "JWT" // Wrong typ
646
+
token.Header["jwk"] = key.jwk
648
+
proof, err := token.SignedString(key.privateKey)
650
+
t.Fatalf("Failed to create test proof: %v", err)
653
+
_, err = verifier.VerifyDPoPProof(proof, method, uri)
655
+
t.Error("Expected error for wrong typ header, got nil")
657
+
if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
658
+
t.Errorf("Expected typ header error, got: %v", err)
662
+
func TestVerifyDPoPProof_MissingJWK(t *testing.T) {
663
+
verifier := NewDPoPVerifier()
664
+
key := generateTestES256Key(t)
667
+
uri := "https://api.example.com/resource"
669
+
jti := uuid.New().String()
671
+
claims := &DPoPClaims{
672
+
RegisteredClaims: jwt.RegisteredClaims{
674
+
IssuedAt: jwt.NewNumericDate(iat),
676
+
HTTPMethod: method,
680
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
681
+
token.Header["typ"] = "dpop+jwt"
682
+
// Don't include JWK
684
+
proof, err := token.SignedString(key.privateKey)
686
+
t.Fatalf("Failed to create test proof: %v", err)
689
+
_, err = verifier.VerifyDPoPProof(proof, method, uri)
691
+
t.Error("Expected error for missing jwk header, got nil")
693
+
if err != nil && !contains(err.Error(), "missing jwk") {
694
+
t.Errorf("Expected missing jwk error, got: %v", err)
698
+
func TestVerifyDPoPProof_CustomTimeSettings(t *testing.T) {
699
+
verifier := &DPoPVerifier{
700
+
MaxClockSkew: 1 * time.Minute,
701
+
MaxProofAge: 10 * time.Minute,
703
+
key := generateTestES256Key(t)
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()
711
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
713
+
_, err := verifier.VerifyDPoPProof(proof, method, uri)
715
+
t.Fatalf("VerifyDPoPProof failed with custom time settings: %v", err)
719
+
func TestVerifyDPoPProof_HTTPMethodCaseInsensitive(t *testing.T) {
720
+
// HTTP method comparison should be case-insensitive per spec
721
+
verifier := NewDPoPVerifier()
722
+
key := generateTestES256Key(t)
725
+
uri := "https://api.example.com/resource"
727
+
jti := uuid.New().String()
729
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
731
+
// Verify with uppercase method
732
+
_, err := verifier.VerifyDPoPProof(proof, "POST", uri)
734
+
t.Fatalf("VerifyDPoPProof failed for case-insensitive method: %v", err)
738
+
// === Token Binding Verification Tests ===
740
+
func TestVerifyTokenBinding_Matching(t *testing.T) {
741
+
verifier := NewDPoPVerifier()
742
+
key := generateTestES256Key(t)
745
+
uri := "https://api.example.com/resource"
747
+
jti := uuid.New().String()
749
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
751
+
result, err := verifier.VerifyDPoPProof(proof, method, uri)
753
+
t.Fatalf("VerifyDPoPProof failed: %v", err)
756
+
// Verify token binding with matching thumbprint
757
+
err = verifier.VerifyTokenBinding(result, key.thumbprint)
759
+
t.Fatalf("VerifyTokenBinding failed for matching thumbprint: %v", err)
763
+
func TestVerifyTokenBinding_Mismatch(t *testing.T) {
764
+
verifier := NewDPoPVerifier()
765
+
key := generateTestES256Key(t)
766
+
wrongKey := generateTestES256Key(t)
769
+
uri := "https://api.example.com/resource"
771
+
jti := uuid.New().String()
773
+
proof := createDPoPProof(t, key, method, uri, iat, jti)
775
+
result, err := verifier.VerifyDPoPProof(proof, method, uri)
777
+
t.Fatalf("VerifyDPoPProof failed: %v", err)
780
+
// Verify token binding with wrong thumbprint
781
+
err = verifier.VerifyTokenBinding(result, wrongKey.thumbprint)
783
+
t.Error("Expected error for thumbprint mismatch, got nil")
785
+
if err != nil && !contains(err.Error(), "thumbprint mismatch") {
786
+
t.Errorf("Expected thumbprint mismatch error, got: %v", err)
790
+
// === ExtractCnfJkt Tests ===
792
+
func TestExtractCnfJkt_Valid(t *testing.T) {
793
+
expectedJkt := "test-thumbprint-123"
795
+
Confirmation: map[string]interface{}{
796
+
"jkt": expectedJkt,
800
+
jkt, err := ExtractCnfJkt(claims)
802
+
t.Fatalf("ExtractCnfJkt failed for valid claims: %v", err)
805
+
if jkt != expectedJkt {
806
+
t.Errorf("Expected jkt %s, got %s", expectedJkt, jkt)
810
+
func TestExtractCnfJkt_MissingCnf(t *testing.T) {
815
+
_, err := ExtractCnfJkt(claims)
817
+
t.Error("Expected error for missing cnf, got nil")
819
+
if err != nil && !contains(err.Error(), "missing cnf claim") {
820
+
t.Errorf("Expected missing cnf error, got: %v", err)
824
+
func TestExtractCnfJkt_NilCnf(t *testing.T) {
829
+
_, err := ExtractCnfJkt(claims)
831
+
t.Error("Expected error for nil cnf, got nil")
833
+
if err != nil && !contains(err.Error(), "missing cnf claim") {
834
+
t.Errorf("Expected missing cnf error, got: %v", err)
838
+
func TestExtractCnfJkt_MissingJkt(t *testing.T) {
840
+
Confirmation: map[string]interface{}{
845
+
_, err := ExtractCnfJkt(claims)
847
+
t.Error("Expected error for missing jkt, got nil")
849
+
if err != nil && !contains(err.Error(), "missing jkt") {
850
+
t.Errorf("Expected missing jkt error, got: %v", err)
854
+
func TestExtractCnfJkt_EmptyJkt(t *testing.T) {
856
+
Confirmation: map[string]interface{}{
861
+
_, err := ExtractCnfJkt(claims)
863
+
t.Error("Expected error for empty jkt, got nil")
865
+
if err != nil && !contains(err.Error(), "missing jkt") {
866
+
t.Errorf("Expected missing jkt error, got: %v", err)
870
+
func TestExtractCnfJkt_WrongType(t *testing.T) {
872
+
Confirmation: map[string]interface{}{
873
+
"jkt": 123, // Not a string
877
+
_, err := ExtractCnfJkt(claims)
879
+
t.Error("Expected error for wrong type jkt, got nil")
881
+
if err != nil && !contains(err.Error(), "missing jkt") {
882
+
t.Errorf("Expected missing jkt error, got: %v", err)
886
+
// === Helper Functions for Tests ===
888
+
// splitJWT splits a JWT into its three parts
889
+
func splitJWT(token string) []string {
891
+
token[:strings.IndexByte(token, '.')],
892
+
token[strings.IndexByte(token, '.')+1 : strings.LastIndexByte(token, '.')],
893
+
token[strings.LastIndexByte(token, '.')+1:],
897
+
// parseJWTHeader parses a base64url-encoded JWT header
898
+
func parseJWTHeader(t *testing.T, encoded string) map[string]interface{} {
900
+
decoded, err := base64.RawURLEncoding.DecodeString(encoded)
902
+
t.Fatalf("Failed to decode header: %v", err)
905
+
var header map[string]interface{}
906
+
if err := json.Unmarshal(decoded, &header); err != nil {
907
+
t.Fatalf("Failed to unmarshal header: %v", err)
913
+
// encodeJSON encodes a value to base64url-encoded JSON
914
+
func encodeJSON(t *testing.T, v interface{}) string {
916
+
data, err := json.Marshal(v)
918
+
t.Fatalf("Failed to marshal JSON: %v", err)
920
+
return base64.RawURLEncoding.EncodeToString(data)