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}