github.com/avenga/couper@v1.12.2/server/http_oauth2_test.go (about) 1 package server_test 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "net/url" 12 "os" 13 "reflect" 14 "regexp" 15 "strconv" 16 "strings" 17 "sync" 18 "sync/atomic" 19 "testing" 20 "time" 21 22 "github.com/golang-jwt/jwt/v4" 23 "github.com/sirupsen/logrus" 24 logrustest "github.com/sirupsen/logrus/hooks/test" 25 26 "github.com/avenga/couper/cache" 27 "github.com/avenga/couper/config/configload" 28 "github.com/avenga/couper/config/runtime" 29 "github.com/avenga/couper/errors" 30 "github.com/avenga/couper/eval/lib" 31 "github.com/avenga/couper/internal/test" 32 "github.com/avenga/couper/logging" 33 "github.com/avenga/couper/oauth2" 34 ) 35 36 func TestEndpoints_OAuth2(t *testing.T) { 37 helper := test.New(t) 38 39 for i := range []int{0, 1, 2} { 40 var seenCh, tokenSeenCh chan struct{} 41 42 retries := 0 43 44 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 45 if req.URL.Path == "/oauth2" { 46 if accept := req.Header.Get("Accept"); accept != "application/json" { 47 t.Errorf("expected Accept %q, got: %q", "application/json", accept) 48 } 49 50 rw.Header().Set("Content-Type", "application/json") 51 rw.WriteHeader(http.StatusOK) 52 53 body := []byte(`{ 54 "access_token": "abcdef0123456789", 55 "token_type": "bearer", 56 "expires_in": 100 57 }`) 58 _, werr := rw.Write(body) 59 helper.Must(werr) 60 61 // retries must be equal with the number of retries in the `testdata/oauth2/XXX_retries_couper.hcl` 62 if retries == i { 63 close(tokenSeenCh) 64 } 65 66 return 67 } 68 rw.WriteHeader(http.StatusBadRequest) 69 })) 70 defer oauthOrigin.Close() 71 72 ResourceOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 73 if req.URL.Path == "/resource" { 74 // retries must be equal with the number of retries in the `testdata/oauth2/XXX_retries_couper.hcl` 75 if req.Header.Get("Authorization") == "Bearer abcdef0123456789" && retries == i { 76 rw.WriteHeader(http.StatusNoContent) 77 close(seenCh) 78 return 79 } 80 81 retries++ 82 83 rw.WriteHeader(http.StatusUnauthorized) 84 return 85 } 86 87 rw.WriteHeader(http.StatusNotFound) 88 })) 89 defer ResourceOrigin.Close() 90 91 confPath := fmt.Sprintf("testdata/oauth2/%d_retries_couper.hcl", i) 92 shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL, "rsOrigin": ResourceOrigin.URL}) 93 helper.Must(err) 94 defer shutdown() 95 96 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil) 97 helper.Must(err) 98 99 for _, p := range []string{"/", "/2nd", "/password"} { 100 hook.Reset() 101 102 seenCh = make(chan struct{}) 103 tokenSeenCh = make(chan struct{}) 104 105 req.URL.Path = p 106 res, err := newClient().Do(req) 107 helper.Must(err) 108 109 if res.StatusCode != http.StatusNoContent { 110 t.Errorf("expected status NoContent, got: %d", res.StatusCode) 111 return 112 } 113 114 timer := time.NewTimer(time.Second * 2) 115 select { 116 case <-timer.C: 117 t.Error("OAuth2 request failed") 118 case <-tokenSeenCh: 119 <-seenCh 120 } 121 } 122 123 oauthOrigin.Close() 124 ResourceOrigin.Close() 125 shutdown() 126 } 127 } 128 129 func Test_OAuth2_no_retry(t *testing.T) { 130 // tests that actually no retry is attempted for oauth2 with retries = 0 131 helper := test.New(t) 132 133 retries := 0 134 135 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 136 if req.URL.Path == "/oauth2" { 137 if accept := req.Header.Get("Accept"); accept != "application/json" { 138 t.Errorf("expected Accept %q, got: %q", "application/json", accept) 139 } 140 141 rw.Header().Set("Content-Type", "application/json") 142 rw.WriteHeader(http.StatusOK) 143 144 body := []byte(`{ 145 "access_token": "abcdef0123456789", 146 "token_type": "bearer", 147 "expires_in": 100 148 }`) 149 _, werr := rw.Write(body) 150 helper.Must(werr) 151 152 return 153 } 154 rw.WriteHeader(http.StatusBadRequest) 155 })) 156 defer oauthOrigin.Close() 157 158 ResourceOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 159 if req.URL.Path == "/resource" { 160 if retries > 0 { 161 t.Fatal("Must not retry") 162 } 163 164 retries++ 165 166 rw.WriteHeader(http.StatusUnauthorized) 167 return 168 } 169 170 rw.WriteHeader(http.StatusNotFound) 171 })) 172 defer ResourceOrigin.Close() 173 174 confPath := "testdata/oauth2/0_retries_couper.hcl" 175 shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL, "rsOrigin": ResourceOrigin.URL}) 176 helper.Must(err) 177 defer shutdown() 178 179 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil) 180 helper.Must(err) 181 182 hook.Reset() 183 184 req.URL.Path = "/" 185 res, err := newClient().Do(req) 186 helper.Must(err) 187 188 if res.StatusCode != http.StatusUnauthorized { 189 t.Errorf("expected status %d, got: %d", http.StatusUnauthorized, res.StatusCode) 190 return 191 } 192 193 oauthOrigin.Close() 194 ResourceOrigin.Close() 195 shutdown() 196 } 197 198 func TestEndpoints_OAuth2_Options(t *testing.T) { 199 helper := test.New(t) 200 201 type testCase struct { 202 configFile string 203 expBody string 204 expAuth string 205 } 206 207 for _, tc := range []testCase{ 208 { 209 "01_couper.hcl", 210 `client_id=user&client_secret=pass+word&grant_type=client_credentials&scope=scope1+scope2`, 211 "", 212 }, 213 { 214 "02_couper.hcl", 215 `grant_type=client_credentials`, 216 "Basic dXNlcjpwYXNzJTJCK3dvcmQ=", 217 }, 218 { 219 "03_couper.hcl", 220 `grant_type=client_credentials`, 221 "Basic dXNlcjpwYXNz", 222 }, 223 { 224 "12_couper.hcl", 225 `grant_type=password&password=pass&scope=scope1+scope2&username=user`, 226 "Basic bXlfY2xpZW50Om15X2NsaWVudF9zZWNyZXQ=", 227 }, 228 { 229 "13_couper.hcl", 230 `client_id=my_client&client_secret=my_client_secret&grant_type=password&password=pass&scope=scope1+scope2&username=user`, 231 "", 232 }, 233 { 234 "16_couper.hcl", 235 `assertion=GET&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer`, 236 "", 237 }, 238 } { 239 var tokenSeenCh chan struct{} 240 241 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 242 if req.URL.Path == "/options" { 243 reqBody, _ := io.ReadAll(req.Body) 244 authorization := req.Header.Get("Authorization") 245 246 if tc.expBody != string(reqBody) { 247 t.Errorf("want\n%s\ngot\n%s", tc.expBody, reqBody) 248 } 249 if tc.expAuth != authorization { 250 t.Errorf("want\n%s\ngot\n%s", tc.expAuth, authorization) 251 } 252 253 rw.WriteHeader(http.StatusNoContent) 254 255 close(tokenSeenCh) 256 return 257 } 258 rw.WriteHeader(http.StatusBadRequest) 259 })) 260 defer oauthOrigin.Close() 261 262 confPath := fmt.Sprintf("testdata/oauth2/%s", tc.configFile) 263 shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL}) 264 helper.Must(err) 265 defer shutdown() 266 267 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil) 268 helper.Must(err) 269 270 hook.Reset() 271 272 tokenSeenCh = make(chan struct{}) 273 274 req.URL.Path = "/" 275 _, err = newClient().Do(req) 276 helper.Must(err) 277 278 timer := time.NewTimer(time.Second * 2) 279 select { 280 case <-timer.C: 281 t.Error("OAuth2 request failed") 282 case <-tokenSeenCh: 283 } 284 285 oauthOrigin.Close() 286 shutdown() 287 } 288 } 289 290 func TestEndpoints_OAuth2_JWTBearer(t *testing.T) { 291 helper := test.New(t) 292 293 type testCase struct { 294 name string 295 configFile string 296 assHeader string 297 } 298 jwtParser := jwt.NewParser() 299 keyFunc := func(_ *jwt.Token) (interface{}, error) { 300 return []byte("asdf"), nil 301 } 302 now := time.Now().Unix() 303 passedAssertion, err := lib.CreateJWT("HS256", []byte("asdf"), jwt.MapClaims{"aud": "https://authz.server/token", "exp": now + 10, "iat": now, "iss": "foo@example.com", "scope": "sc1 sc2"}, nil) 304 helper.Must(err) 305 306 expClaims := map[string]interface{}{"aud": "https://authz.server/token", "exp": nil, "iat": nil, "iss": "foo@example.com", "scope": "sc1 sc2"} 307 expGrantType := "urn:ietf:params:oauth:grant-type:jwt-bearer" 308 309 for _, tc := range []testCase{ 310 { 311 "assertion attribute with jwt_sign()", 312 "21_couper.hcl", 313 "", 314 }, 315 { 316 "inline jwt_signing_profile", 317 "22_couper.hcl", 318 "", 319 }, 320 { 321 "passed assertion", 322 "23_couper.hcl", 323 passedAssertion, 324 }, 325 } { 326 var tokenSeenCh chan struct{} 327 328 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 329 if req.URL.Path == "/token" { 330 reqBody, _ := io.ReadAll(req.Body) 331 332 params, err := url.ParseQuery(string(reqBody)) 333 helper.Must(err) 334 335 grantType := params.Get("grant_type") 336 if expGrantType != grantType { 337 t.Errorf("%s: unexpected grant_type: want\n%s\ngot\n%s", tc.name, expGrantType, grantType) 338 } 339 340 assertion := params.Get("assertion") 341 claims := jwt.MapClaims{} 342 _, err = jwtParser.ParseWithClaims(assertion, claims, keyFunc) 343 helper.Must(err) 344 if len(expClaims) != len(claims) { 345 t.Fatalf("%s: unexpected number of claims; want: %d, got: %d", tc.name, len(expClaims), len(claims)) 346 } 347 for k, vExp := range expClaims { 348 v, set := claims[k] 349 if !set { 350 t.Errorf("%s: missing claim %q", tc.name, k) 351 } else { 352 if vExp != nil && vExp != v { 353 t.Errorf("%s: unexpected %s claim value; want: %#v, got: %#v", tc.name, k, vExp, v) 354 } 355 } 356 } 357 358 rw.WriteHeader(http.StatusNoContent) 359 360 close(tokenSeenCh) 361 return 362 } 363 rw.WriteHeader(http.StatusBadRequest) 364 })) 365 defer oauthOrigin.Close() 366 367 confPath := fmt.Sprintf("testdata/oauth2/%s", tc.configFile) 368 shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL}) 369 helper.Must(err) 370 defer shutdown() 371 372 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil) 373 helper.Must(err) 374 if tc.assHeader != "" { 375 req.Header.Add("x-assertion", tc.assHeader) 376 } 377 378 hook.Reset() 379 380 tokenSeenCh = make(chan struct{}) 381 382 req.URL.Path = "/" 383 _, err = newClient().Do(req) 384 helper.Must(err) 385 386 timer := time.NewTimer(time.Second * 2) 387 select { 388 case <-timer.C: 389 t.Error("OAuth2 request failed") 390 case <-tokenSeenCh: 391 } 392 393 oauthOrigin.Close() 394 shutdown() 395 } 396 } 397 398 func TestOAuth2_Config_Errors(t *testing.T) { 399 log, _ := test.NewLogger() 400 401 type testCase struct { 402 name string 403 hcl string 404 error string 405 } 406 407 for _, tc := range []testCase{ 408 { 409 "grant_type client_credentials without client_id", 410 `server {} 411 definitions { 412 backend "be" { 413 oauth2 { 414 token_endpoint = "https://authorization.server/token" 415 client_secret = "my_client_secret" 416 grant_type = "client_credentials" 417 } 418 } 419 } 420 `, 421 "configuration error: be: client_id must not be empty", 422 }, 423 { 424 "grant_type password without client_id", 425 `server {} 426 definitions { 427 backend "be" { 428 oauth2 { 429 token_endpoint = "https://authorization.server/token" 430 client_secret = "my_client_secret" 431 grant_type = "password" 432 username = "my_user" 433 password = "my_password" 434 } 435 } 436 } 437 `, 438 "configuration error: be: client_id must not be empty", 439 }, 440 { 441 "username with grant_type client_credentials", 442 `server {} 443 definitions { 444 backend "be" { 445 oauth2 { 446 token_endpoint = "https://authorization.server/token" 447 client_id = "my_client" 448 client_secret = "my_client_secret" 449 grant_type = "client_credentials" 450 username = "my_user" 451 } 452 } 453 } 454 `, 455 "configuration error: be: username attribute must not be set with grant_type=client_credentials", 456 }, 457 { 458 "password with grant_type client_credentials", 459 `server {} 460 definitions { 461 backend "be" { 462 oauth2 { 463 token_endpoint = "https://authorization.server/token" 464 client_id = "my_client" 465 client_secret = "my_client_secret" 466 grant_type = "client_credentials" 467 password = "my_password" 468 } 469 } 470 } 471 `, 472 "configuration error: be: password attribute must not be set with grant_type=client_credentials", 473 }, 474 { 475 "username with grant_type jwt-bearer", 476 `server {} 477 definitions { 478 backend "be" { 479 oauth2 { 480 token_endpoint = "https://authorization.server/token" 481 grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" 482 username = "my_user" 483 } 484 } 485 } 486 `, 487 "configuration error: be: username attribute must not be set with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer", 488 }, 489 { 490 "password with grant_type jwt-bearer", 491 `server {} 492 definitions { 493 backend "be" { 494 oauth2 { 495 token_endpoint = "https://authorization.server/token" 496 grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" 497 password = "my_password" 498 } 499 } 500 } 501 `, 502 "configuration error: be: password attribute must not be set with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer", 503 }, 504 { 505 "assertion with grant_type client_credentials", 506 `server {} 507 definitions { 508 backend "be" { 509 oauth2 { 510 token_endpoint = "https://authorization.server/token" 511 client_id = "my_client" 512 client_secret = "my_client_secret" 513 grant_type = "client_credentials" 514 assertion = "my_assertion" 515 } 516 } 517 } 518 `, 519 "configuration error: be: assertion attribute must not be set with grant_type=client_credentials", 520 }, 521 { 522 "assertion with grant_type password", 523 `server {} 524 definitions { 525 backend "be" { 526 oauth2 { 527 token_endpoint = "https://authorization.server/token" 528 client_id = "my_client" 529 client_secret = "my_client_secret" 530 grant_type = "password" 531 username = "my_user" 532 password = "my_password" 533 assertion = "my_assertion" 534 } 535 } 536 } 537 `, 538 "configuration error: be: assertion attribute must not be set with grant_type=password", 539 }, 540 { 541 "missing username with grant_type password", 542 `server {} 543 definitions { 544 backend "be" { 545 oauth2 { 546 token_endpoint = "https://authorization.server/token" 547 client_id = "my_client" 548 client_secret = "my_client_secret" 549 grant_type = "password" 550 } 551 } 552 } 553 `, 554 "configuration error: be: username must not be empty with grant_type=password", 555 }, 556 { 557 "missing password with grant_type password", 558 `server {} 559 definitions { 560 backend "be" { 561 oauth2 { 562 token_endpoint = "https://authorization.server/token" 563 client_id = "my_client" 564 client_secret = "my_client_secret" 565 grant_type = "password" 566 username = "my_user" 567 } 568 } 569 } 570 `, 571 "configuration error: be: password must not be empty with grant_type=password", 572 }, 573 { 574 "missing assertion with grant_type jwt-bearer", 575 `server {} 576 definitions { 577 backend "be" { 578 oauth2 { 579 token_endpoint = "https://authorization.server/token" 580 grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" 581 } 582 } 583 } 584 `, 585 "configuration error: be: missing assertion attribute or jwt_signing_profile block with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer", 586 }, 587 588 { 589 "unsupported token_endpoint_auth_method", 590 `server {} 591 definitions { 592 backend "be" { 593 oauth2 { 594 token_endpoint = "https://authorization.server/token" 595 client_id = "my_client" 596 client_secret = "my_client_secret" 597 grant_type = "client_credentials" 598 token_endpoint_auth_method = "unknown" 599 } 600 } 601 } 602 `, 603 `configuration error: be: token_endpoint_auth_method "unknown" not supported`, 604 }, 605 606 { 607 "missing client_secret with client_secret_basic", 608 `server {} 609 definitions { 610 backend "be" { 611 oauth2 { 612 token_endpoint = "https://authorization.server/token" 613 client_id = "my_client" 614 grant_type = "client_credentials" 615 } 616 } 617 } 618 `, 619 "configuration error: be: client_secret must not be empty with client_secret_basic", 620 }, 621 { 622 "missing client_secret with client_secret_post", 623 `server {} 624 definitions { 625 backend "be" { 626 oauth2 { 627 token_endpoint = "https://authorization.server/token" 628 client_id = "my_client" 629 grant_type = "client_credentials" 630 token_endpoint_auth_method = "client_secret_post" 631 } 632 } 633 } 634 `, 635 "configuration error: be: client_secret must not be empty with client_secret_post", 636 }, 637 { 638 "missing client_secret with client_secret_jwt", 639 `server {} 640 definitions { 641 backend "be" { 642 oauth2 { 643 token_endpoint = "https://authorization.server/token" 644 client_id = "my_client" 645 grant_type = "client_credentials" 646 token_endpoint_auth_method = "client_secret_jwt" 647 } 648 } 649 } 650 `, 651 "configuration error: be: client_secret must not be empty with client_secret_jwt", 652 }, 653 { 654 "client_secret with private_key_jwt", 655 `server {} 656 definitions { 657 backend "be" { 658 oauth2 { 659 token_endpoint = "https://authorization.server/token" 660 client_id = "my_client" 661 client_secret = "my_client_secret" 662 grant_type = "client_credentials" 663 token_endpoint_auth_method = "private_key_jwt" 664 } 665 } 666 } 667 `, 668 "configuration error: be: client_secret must not be set with private_key_jwt", 669 }, 670 671 { 672 "jwt_signing_profile with client_secret_basic", 673 `server {} 674 definitions { 675 backend "be" { 676 oauth2 { 677 token_endpoint = "https://authorization.server/token" 678 client_id = "my_client" 679 client_secret = "my_client_secret" 680 grant_type = "client_credentials" 681 token_endpoint_auth_method = "client_secret_basic" 682 jwt_signing_profile { 683 signature_algorithm = "HS256" 684 ttl = "10s" 685 } 686 } 687 } 688 } 689 `, 690 "configuration error: be: jwt_signing_profile block must not be set with client_secret_basic", 691 }, 692 { 693 "jwt_signing_profile with client_secret_post", 694 `server {} 695 definitions { 696 backend "be" { 697 oauth2 { 698 token_endpoint = "https://authorization.server/token" 699 client_id = "my_client" 700 client_secret = "my_client_secret" 701 grant_type = "client_credentials" 702 token_endpoint_auth_method = "client_secret_post" 703 jwt_signing_profile { 704 signature_algorithm = "HS256" 705 ttl = "10s" 706 } 707 } 708 } 709 } 710 `, 711 "configuration error: be: jwt_signing_profile block must not be set with client_secret_post", 712 }, 713 { 714 "inappropriate authn algorithm with client_secret_jwt", 715 `server {} 716 definitions { 717 backend "be" { 718 oauth2 { 719 token_endpoint = "https://authorization.server/token" 720 client_id = "my_client" 721 client_secret = "my_client_secret" 722 grant_type = "client_credentials" 723 token_endpoint_auth_method = "client_secret_jwt" 724 jwt_signing_profile { 725 signature_algorithm = "RS256" 726 ttl = "10s" 727 } 728 } 729 } 730 } 731 `, 732 "configuration error: be: inappropriate signature algorithm with client_secret_jwt", 733 }, 734 { 735 "inappropriate authn algorithm with private_key_jwt", 736 `server {} 737 definitions { 738 backend "be" { 739 oauth2 { 740 token_endpoint = "https://authorization.server/token" 741 client_id = "my_client" 742 grant_type = "client_credentials" 743 token_endpoint_auth_method = "private_key_jwt" 744 jwt_signing_profile { 745 signature_algorithm = "HS256" 746 key = "a key" 747 ttl = "10s" 748 } 749 } 750 } 751 } 752 `, 753 "configuration error: be: inappropriate signature algorithm with private_key_jwt", 754 }, 755 756 { 757 "invalid authn ttl with client_secret_jwt", 758 `server {} 759 definitions { 760 backend "be" { 761 oauth2 { 762 token_endpoint = "https://authorization.server/token" 763 client_id = "my_client" 764 client_secret = "my_client_secret" 765 grant_type = "client_credentials" 766 token_endpoint_auth_method = "client_secret_jwt" 767 jwt_signing_profile { 768 signature_algorithm = "HS256" 769 ttl = "10" 770 } 771 } 772 } 773 } 774 `, 775 `configuration error: be: time: missing unit in duration "10"`, 776 }, 777 { 778 "invalid authn ttl with private_key_jwt", 779 `server {} 780 definitions { 781 backend "be" { 782 oauth2 { 783 token_endpoint = "https://authorization.server/token" 784 client_id = "my_client" 785 grant_type = "client_credentials" 786 token_endpoint_auth_method = "private_key_jwt" 787 jwt_signing_profile { 788 signature_algorithm = "RS256" 789 key = "a key" 790 ttl = "10" 791 } 792 } 793 } 794 } 795 `, 796 `configuration error: be: time: missing unit in duration "10"`, 797 }, 798 799 { 800 "authn key with client_secret_jwt", 801 `server {} 802 definitions { 803 backend "be" { 804 oauth2 { 805 token_endpoint = "https://authorization.server/token" 806 client_id = "my_client" 807 client_secret = "my_client_secret" 808 grant_type = "client_credentials" 809 token_endpoint_auth_method = "client_secret_jwt" 810 jwt_signing_profile { 811 key = "a key" 812 signature_algorithm = "HS256" 813 ttl = "10s" 814 } 815 } 816 } 817 } 818 `, 819 "configuration error: be: key must not be set with client_secret_jwt", 820 }, 821 { 822 "authn key value not being a valid key", 823 `server {} 824 definitions { 825 backend "be" { 826 oauth2 { 827 token_endpoint = "https://authorization.server/token" 828 client_id = "my_client" 829 grant_type = "client_credentials" 830 token_endpoint_auth_method = "private_key_jwt" 831 jwt_signing_profile { 832 signature_algorithm = "RS256" 833 ttl = "10s" 834 key = "not an RSA private key" 835 } 836 } 837 } 838 } 839 `, 840 "configuration error: be: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key", 841 }, 842 843 { 844 "authn key_file with client_secret_jwt", 845 `server {} 846 definitions { 847 backend "be" { 848 oauth2 { 849 token_endpoint = "https://authorization.server/token" 850 client_id = "my_client" 851 client_secret = "my_client_secret" 852 grant_type = "client_credentials" 853 token_endpoint_auth_method = "client_secret_jwt" 854 jwt_signing_profile { 855 key_file = "a_key_file" 856 signature_algorithm = "HS256" 857 ttl = "10s" 858 } 859 } 860 } 861 } 862 `, 863 "configuration error: be: key_file must not be set with client_secret_jwt", 864 }, 865 { 866 "missing authn key/key_file with private_key_jwt", 867 `server {} 868 definitions { 869 backend "be" { 870 oauth2 { 871 token_endpoint = "https://authorization.server/token" 872 client_id = "my_client" 873 grant_type = "client_credentials" 874 token_endpoint_auth_method = "private_key_jwt" 875 jwt_signing_profile { 876 signature_algorithm = "RS256" 877 ttl = "10s" 878 } 879 } 880 } 881 } 882 `, 883 "configuration error: be: key and key_file must not both be empty with private_key_jwt", 884 }, 885 { 886 "key_file referencing non-existing file", 887 `server {} 888 definitions { 889 backend "be" { 890 oauth2 { 891 token_endpoint = "https://authorization.server/token" 892 client_id = "my_client" 893 grant_type = "client_credentials" 894 token_endpoint_auth_method = "private_key_jwt" 895 jwt_signing_profile { 896 signature_algorithm = "RS256" 897 ttl = "10s" 898 key_file = "unknown" 899 } 900 } 901 } 902 } 903 `, 904 "configuration error: be: jwt_signing_profile key: read error: open ", 905 }, 906 { 907 "alg header with client_secret_jwt", 908 `server {} 909 definitions { 910 backend "be" { 911 oauth2 { 912 token_endpoint = "https://authorization.server/token" 913 client_id = "my_client" 914 client_secret = "my_client_secret" 915 grant_type = "client_credentials" 916 token_endpoint_auth_method = "client_secret_jwt" 917 jwt_signing_profile { 918 signature_algorithm = "HS256" 919 ttl = "10s" 920 headers = { 921 alg = "some value" 922 } 923 } 924 } 925 } 926 } 927 `, 928 "configuration error: be: \"alg\" cannot be set via \"headers\"", 929 }, 930 } { 931 var errMsg string 932 conf, err := configload.LoadBytes([]byte(tc.hcl), "couper.hcl") 933 if conf != nil { 934 logger := log.WithContext(context.TODO()) 935 936 tmpStoreCh := make(chan struct{}) 937 defer close(tmpStoreCh) 938 939 ctx, cancel := context.WithCancel(conf.Context) 940 conf.Context = ctx 941 defer cancel() 942 943 _, err = runtime.NewServerConfiguration(conf, logger, cache.New(logger, tmpStoreCh)) 944 } 945 946 if err != nil { 947 if _, ok := err.(errors.GoError); ok { 948 errMsg = err.(errors.GoError).LogError() 949 } else { 950 errMsg = err.Error() 951 } 952 } 953 954 if !strings.HasPrefix(errMsg, tc.error) { 955 t.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot: %q", tc.name, tc.error, errMsg) 956 } 957 } 958 } 959 960 func TestOAuth2_AuthnJWT(t *testing.T) { 961 helper := test.New(t) 962 jtiRE, err := regexp.Compile("^[a-zA-Z0-9]{43}$") 963 helper.Must(err) 964 965 rsOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 966 authz := req.Header.Get("Authorization") 967 if !strings.HasPrefix(authz, "Bearer ") { 968 helper.Must(fmt.Errorf("wrong authz: %q", authz)) 969 } 970 token := strings.TrimPrefix(authz, "Bearer ") 971 parts := strings.Split(token, " ") 972 if len(parts) != 3 { 973 helper.Must(fmt.Errorf("wrong token: %q", token)) 974 } 975 exp, err := strconv.Atoi(parts[1]) 976 helper.Must(err) 977 iat, err := strconv.Atoi(parts[0]) 978 helper.Must(err) 979 if exp-iat != 10 { 980 helper.Must(fmt.Errorf("wrong token: %q", token)) 981 } 982 if !jtiRE.MatchString(parts[2]) { 983 helper.Must(fmt.Errorf("wrong jti: %q", parts[2])) 984 } 985 rw.WriteHeader(http.StatusNoContent) 986 })) 987 defer rsOrigin.Close() 988 989 type testCase struct { 990 name string 991 path string 992 wantStatus int 993 wantErrLog string 994 } 995 996 for _, tc := range []testCase{ 997 { 998 "client_secret_jwt", 999 "/csj", 1000 http.StatusNoContent, 1001 "", 1002 }, 1003 { 1004 "client_secret_jwt error", 1005 "/csj_error", 1006 http.StatusBadGateway, 1007 "access control error: csj_error: signature is invalid", 1008 }, 1009 { 1010 "private_key_jwt", 1011 "/pkj", 1012 http.StatusNoContent, 1013 "", 1014 }, 1015 { 1016 "private_key_jwt error", 1017 "/pkj_error", 1018 http.StatusBadGateway, 1019 "access control error: pkj_error: signing method RS256 is invalid", 1020 }, 1021 } { 1022 t.Run(tc.name, func(subT *testing.T) { 1023 h := test.New(subT) 1024 1025 shutdown, hook, err := newCouperWithTemplate("testdata/oauth2/20_couper.hcl", h, map[string]interface{}{"rsOrigin": rsOrigin.URL}) 1026 h.Must(err) 1027 defer shutdown() 1028 1029 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080"+tc.path, nil) 1030 h.Must(err) 1031 1032 hook.Reset() 1033 1034 res, err := newClient().Do(req) 1035 h.Must(err) 1036 1037 if res.StatusCode != tc.wantStatus { 1038 t.Errorf("expected status %d, got: %d", tc.wantStatus, res.StatusCode) 1039 } 1040 1041 message := getFirstAccessLogMessage(hook) 1042 if message != tc.wantErrLog { 1043 t.Errorf("error log\nwant: %q\ngot: %q", tc.wantErrLog, message) 1044 } 1045 1046 shutdown() 1047 }) 1048 } 1049 1050 rsOrigin.Close() 1051 } 1052 1053 func TestOAuth2_Runtime_Errors(t *testing.T) { 1054 helper := test.New(t) 1055 1056 asOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1057 if req.URL.Path == "/token" { 1058 rw.Header().Set("Content-Type", "application/json") 1059 rw.WriteHeader(http.StatusOK) 1060 1061 body := []byte(`{ 1062 "access_token": "abcdef0123456789", 1063 "token_type": "bearer", 1064 "expires_in": 100 1065 }`) 1066 _, werr := rw.Write(body) 1067 helper.Must(werr) 1068 return 1069 } 1070 rw.WriteHeader(http.StatusBadRequest) 1071 })) 1072 defer asOrigin.Close() 1073 1074 type testCase struct { 1075 name string 1076 filename string 1077 wantErrLog string 1078 } 1079 1080 for _, tc := range []testCase{ 1081 {"null assertion", "17_couper.hcl", "backend error: be: request error: oauth2: assertion expression evaluates to null"}, 1082 {"non-string assertion", "18_couper.hcl", "backend error: be: request error: oauth2: assertion expression must evaluate to a string"}, 1083 {"token request error", "19_couper.hcl", "backend error: be: request error: oauth2: token request failed"}, 1084 } { 1085 t.Run(tc.name, func(subT *testing.T) { 1086 h := test.New(subT) 1087 1088 shutdown, hook, err := newCouperWithTemplate("testdata/oauth2/"+tc.filename, h, map[string]interface{}{"asOrigin": asOrigin.URL}) 1089 h.Must(err) 1090 defer shutdown() 1091 1092 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/resource", nil) 1093 h.Must(err) 1094 1095 hook.Reset() 1096 1097 res, err := newClient().Do(req) 1098 h.Must(err) 1099 1100 if res.StatusCode != http.StatusBadGateway { 1101 t.Errorf("expected status StatusBadGateway, got: %d", res.StatusCode) 1102 } 1103 1104 message := getFirstAccessLogMessage(hook) 1105 if message != tc.wantErrLog { 1106 t.Errorf("error log\nwant: %q\ngot: %q", tc.wantErrLog, message) 1107 } 1108 1109 shutdown() 1110 }) 1111 } 1112 1113 asOrigin.Close() 1114 } 1115 1116 func TestOAuth2_AccessControl(t *testing.T) { 1117 client := newClient() 1118 1119 st := "qeirtbnpetrbi" 1120 state := oauth2.Base64urlSha256(st) 1121 1122 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1123 errResp := func(err error) { 1124 rw.WriteHeader(http.StatusInternalServerError) 1125 _, _ = rw.Write([]byte(err.Error())) 1126 } 1127 1128 if req.URL.Path == "/token" { 1129 if accept := req.Header.Get("Accept"); accept != "application/json" { 1130 t.Errorf("expected Accept %q, got: %q", "application/json", accept) 1131 } 1132 _ = req.ParseForm() 1133 rw.Header().Set("Content-Type", "application/json") 1134 rw.WriteHeader(http.StatusOK) 1135 1136 code := req.PostForm.Get("code") 1137 idTokenToAdd := "" 1138 if strings.HasSuffix(code, "-id") { 1139 nonce := state 1140 mapClaims := jwt.MapClaims{} 1141 if !strings.HasSuffix(code, "-maud-id") { 1142 if strings.HasSuffix(code, "-waud-id") { 1143 mapClaims["aud"] = "another-client-id" 1144 } else if strings.HasSuffix(code, "-naud-id") { 1145 mapClaims["aud"] = nil 1146 } else { 1147 mapClaims["aud"] = []string{"foo", "another-client-id"} 1148 } 1149 } 1150 if !strings.HasSuffix(code, "-miss-id") { 1151 if strings.HasSuffix(code, "-wiss-id") { 1152 mapClaims["iss"] = "https://malicious.authorization.server" 1153 } else if strings.HasSuffix(code, "-wuiss-id") { 1154 mapClaims["iss"] = "https://authorization.server/without/userinfo" 1155 } else { 1156 mapClaims["iss"] = "https://authorization.server" 1157 } 1158 } 1159 if !strings.HasSuffix(code, "-miat-id") { 1160 if strings.HasSuffix(code, "-wiat-id") { 1161 mapClaims["iat"] = "1234abcd" 1162 } else if strings.HasSuffix(code, "-liat-id") { 1163 // 2096-10-02 07:06:40 +0000 UTC 1164 mapClaims["iat"] = 4000000000 1165 } else { 1166 // 1970-01-01 00:16:40 +0000 UTC 1167 mapClaims["iat"] = 1000 1168 } 1169 } 1170 if !strings.HasSuffix(code, "-mexp-id") { 1171 if strings.HasSuffix(code, "-wexp-id") { 1172 mapClaims["exp"] = "1234abcd" 1173 } else if strings.HasSuffix(code, "-eexp-id") { 1174 // 1970-01-01 00:16:40 +0000 UTC 1175 mapClaims["exp"] = 1000 1176 } else { 1177 // 2096-10-02 07:06:40 +0000 UTC 1178 mapClaims["exp"] = 4000000000 1179 } 1180 } 1181 if !strings.HasSuffix(code, "-msub-id") { 1182 if strings.HasSuffix(code, "-wsub-id") { 1183 mapClaims["sub"] = "me" 1184 } else { 1185 mapClaims["sub"] = "myself" 1186 } 1187 } 1188 if strings.HasSuffix(code, "-wazp-id") { 1189 mapClaims["azp"] = "bar" 1190 } else if !strings.HasSuffix(code, "-mazp-id") { 1191 mapClaims["azp"] = "foo" 1192 } 1193 if strings.HasSuffix(code, "-wn-id") { 1194 nonce = nonce + "-wrong" 1195 } 1196 if !strings.HasSuffix(code, "-mn-id") { 1197 mapClaims["nonce"] = nonce 1198 } 1199 keyBytes, err := os.ReadFile("testdata/integration/files/pkcs8.key") 1200 if err != nil { 1201 errResp(err) 1202 return 1203 } 1204 1205 key, parseErr := jwt.ParseRSAPrivateKeyFromPEM(keyBytes) 1206 if parseErr != nil { 1207 errResp(err) 1208 return 1209 } 1210 1211 var kid string 1212 if strings.HasSuffix(code, "-wkid-id") { 1213 kid = "not-found" 1214 } else { 1215 kid = "rs256" 1216 } 1217 1218 idToken, err := lib.CreateJWT("RS256", key, mapClaims, map[string]interface{}{"kid": kid}) 1219 if err != nil { 1220 errResp(err) 1221 return 1222 } 1223 1224 idTokenToAdd = `"id_token":"` + idToken + `", 1225 ` 1226 } 1227 1228 body := []byte(`{ 1229 "access_token": "abcdef0123456789", 1230 "token_type": "bearer", 1231 "expires_in": 100, 1232 ` + idTokenToAdd + 1233 `"form_params": "` + req.PostForm.Encode() + `", 1234 "authorization": "` + req.Header.Get("Authorization") + `" 1235 }`) 1236 _, werr := rw.Write(body) 1237 if werr != nil { 1238 t.Log(werr) 1239 } 1240 1241 return 1242 } else if req.URL.Path == "/userinfo" { 1243 body := []byte(`{"sub": "myself"}`) 1244 _, werr := rw.Write(body) 1245 if werr != nil { 1246 t.Log(werr) 1247 } 1248 1249 return 1250 } else if req.URL.Path == "/jwks" { 1251 jsonBytes, rerr := os.ReadFile("testdata/integration/files/jwks.json") 1252 if rerr != nil { 1253 errResp(rerr) 1254 return 1255 } 1256 b := bytes.NewBuffer(jsonBytes) 1257 _, werr := b.WriteTo(rw) 1258 if werr != nil { 1259 t.Log(werr) 1260 } 1261 1262 return 1263 } else if req.URL.Path == "/.well-known/openid-configuration" { 1264 body := []byte(`{ 1265 "issuer": "https://authorization.server", 1266 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 1267 "jwks_uri": "http://` + req.Host + `/jwks", 1268 "token_endpoint": "http://` + req.Host + `/token", 1269 "userinfo_endpoint": "http://` + req.Host + `/userinfo" 1270 }`) 1271 _, werr := rw.Write(body) 1272 if werr != nil { 1273 t.Log(werr) 1274 } 1275 return 1276 } else if req.URL.Path == "/without/userinfo/.well-known/openid-configuration" { 1277 body := []byte(`{ 1278 "issuer": "https://authorization.server/without/userinfo", 1279 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 1280 "jwks_uri": "http://` + req.Host + `/jwks", 1281 "token_endpoint": "http://` + req.Host + `/token" 1282 }`) 1283 _, werr := rw.Write(body) 1284 if werr != nil { 1285 t.Log(werr) 1286 } 1287 return 1288 } 1289 rw.WriteHeader(http.StatusBadRequest) 1290 })) 1291 defer oauthOrigin.Close() 1292 1293 type testCase struct { 1294 name string 1295 filename string 1296 method string 1297 path string 1298 header http.Header 1299 status int 1300 params string 1301 authorization string 1302 wantErrLog string 1303 } 1304 1305 for _, tc := range []testCase{ 1306 {"wrong method", "04_couper.hcl", http.MethodPost, "/cb?code=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}}, http.StatusForbidden, "", "", "access control error: ac: wrong method (POST)"}, 1307 {"oidc: wrong method", "07_couper.hcl", http.MethodPost, "/cb?code=qeuboub-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: wrong method (POST)"}, 1308 {"no code, but error", "04_couper.hcl", http.MethodGet, "/cb?error=qeuboub", http.Header{}, http.StatusForbidden, "", "", "access control error: ac: missing code query parameter; query=\"error=qeuboub\""}, 1309 {"no code; error handler", "05_couper.hcl", http.MethodGet, "/cb?error=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}}, http.StatusTeapot, "", "", "access control error: ac: missing code query parameter; query=\"error=qeuboub\""}, 1310 {"oidc: no code; error handler", "10_couper.hcl", http.MethodGet, "/cb?error=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}}, http.StatusTeapot, "", "", "access control error: ac: missing code query parameter; query=\"error=qeuboub\""}, 1311 {"code, missing state param", "06_couper.hcl", http.MethodGet, "/cb?code=qeuboub", http.Header{"Cookie": []string{"st=qerbnr"}}, http.StatusForbidden, "", "", "access control error: ac: missing state query parameter; query=\"code=qeuboub\""}, 1312 {"code, wrong state param", "06_couper.hcl", http.MethodGet, "/cb?code=qeuboub&state=wrong", http.Header{"Cookie": []string{"st=" + st}}, http.StatusForbidden, "", "", "access control error: ac: state mismatch: \"wrong\" (from query param) vs. \"oUuoMU0RFWI5itMBnMTt_TJ4SxxgE96eZFMNXSl63xQ\" (verifier_value: \"qeirtbnpetrbi\")"}, 1313 {"code, state param, wrong CSRF token", "06_couper.hcl", http.MethodGet, "/cb?code=qeuboub&state=" + state, http.Header{"Cookie": []string{"st=" + st + "-wrong"}}, http.StatusForbidden, "", "", "access control error: ac: state mismatch: \"oUuoMU0RFWI5itMBnMTt_TJ4SxxgE96eZFMNXSl63xQ\" (from query param) vs. \"Mj0ecDMNNzOwqUt1iFlY8TOTTKa17ISo8ARgt0pyb1A\" (verifier_value: \"qeirtbnpetrbi-wrong\")"}, 1314 {"code, state param, missing CSRF token", "06_couper.hcl", http.MethodGet, "/cb?code=qeuboub&state=" + state, http.Header{}, http.StatusForbidden, "", "", "access control error: ac: Empty verifier_value"}, 1315 {"code, missing nonce", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-mn-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: missing nonce claim in ID token"}, 1316 {"code, wrong nonce", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wn-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: nonce mismatch: \"oUuoMU0RFWI5itMBnMTt_TJ4SxxgE96eZFMNXSl63xQ-wrong\" (from nonce claim) vs. \"oUuoMU0RFWI5itMBnMTt_TJ4SxxgE96eZFMNXSl63xQ\" (verifier_value: \"qeirtbnpetrbi\")"}, 1317 {"code, nonce, wrong CSRF token", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-id", http.Header{"Cookie": []string{"nnc=" + st + "-wrong"}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: nonce mismatch: \"oUuoMU0RFWI5itMBnMTt_TJ4SxxgE96eZFMNXSl63xQ\" (from nonce claim) vs. \"Mj0ecDMNNzOwqUt1iFlY8TOTTKa17ISo8ARgt0pyb1A\" (verifier_value: \"qeirtbnpetrbi-wrong\")"}, 1318 {"code, nonce, missing CSRF token", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-id", http.Header{}, http.StatusForbidden, "", "", "access control error: ac: Empty verifier_value"}, 1319 {"code, missing sub claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-msub-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: missing sub claim in ID token"}, 1320 {"code, sub mismatch", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wsub-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: subject mismatch, in ID token \"me\", in userinfo response \"myself\""}, 1321 {"code, missing exp claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-mexp-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: missing exp claim in ID token"}, 1322 {"code, wrong exp claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wexp-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: Token is expired"}, 1323 {"code, too early exp date", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-eexp-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: Token is expired"}, 1324 {"code, missing iat claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-miat-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: missing iat claim in ID token"}, 1325 {"code, wrong iat claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wiat-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: Token used before issued"}, 1326 {"code, too late iat date", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-liat-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: Token used before issued"}, 1327 {"code, missing azp claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-mazp-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: missing azp claim in ID token"}, 1328 {"code, wrong azp claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wazp-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: azp claim / client ID mismatch, azp = \"bar\", client ID = \"foo\""}, 1329 {"code, missing iss claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-miss-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: invalid issuer in ID token"}, 1330 {"code, wrong iss claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wiss-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: invalid issuer in ID token"}, 1331 {"code, missing aud claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-maud-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: invalid audience in ID token"}, 1332 {"code, null aud claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-naud-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: invalid audience in ID token"}, 1333 {"code, wrong aud claim", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-waud-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: invalid audience in ID token"}, 1334 {"code, wrong kid", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wkid-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusForbidden, "", "", "access control error: ac: token response validation error: no matching RS256 JWK for kid \"not-found\""}, 1335 {"code; client_secret_basic; PKCE", "04_couper.hcl", http.MethodGet, "/cb?code=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}}, http.StatusOK, "code=qeuboub&code_verifier=qerbnr&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""}, 1336 {"code; client_secret_post", "05_couper.hcl", http.MethodGet, "/cb?code=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}}, http.StatusOK, "client_id=foo&client_secret=etbinbp4in&code=qeuboub&code_verifier=qerbnr&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "", ""}, 1337 {"code, state param", "06_couper.hcl", http.MethodGet, "/cb?code=qeuboub&state=" + state, http.Header{"Cookie": []string{"st=" + st}}, http.StatusOK, "code=qeuboub&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""}, 1338 {"code, nonce param", "07_couper.hcl", http.MethodGet, "/cb?code=qeuboub-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusOK, "code=qeuboub-id&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""}, 1339 {"code; client_secret_basic; PKCE; relative redirect_uri", "08_couper.hcl", http.MethodGet, "/cb?code=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"www.example.com"}}, http.StatusOK, "code=qeuboub&code_verifier=qerbnr&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fwww.example.com%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""}, 1340 {"code; nonce param; relative redirect_uri", "09_couper.hcl", http.MethodGet, "/cb?code=qeuboub-id", http.Header{"Cookie": []string{"nnc=" + st}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"www.example.com"}}, http.StatusOK, "code=qeuboub-id&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fwww.example.com%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""}, 1341 {"code; without userinfo", "24_couper.hcl", http.MethodGet, "/cb?code=qeuboub-wuiss-id", http.Header{"Cookie": []string{"nnc=" + st}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"www.example.com"}}, http.StatusOK, "code=qeuboub-wuiss-id&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""}, 1342 } { 1343 t.Run(tc.path[1:], func(subT *testing.T) { 1344 h := test.New(subT) 1345 1346 shutdown, hook, err := newCouperWithTemplate("testdata/oauth2/"+tc.filename, h, map[string]interface{}{"asOrigin": oauthOrigin.URL}) 1347 h.Must(err) 1348 defer shutdown() 1349 1350 req, err := http.NewRequest(tc.method, "http://back.end:8080"+tc.path, nil) 1351 h.Must(err) 1352 1353 for k, v := range tc.header { 1354 req.Header.Set(k, v[0]) 1355 } 1356 1357 res, err := client.Do(req) 1358 h.Must(err) 1359 1360 if res.StatusCode != tc.status { 1361 subT.Errorf("%q: expected Status %d, got: %d", tc.name, tc.status, res.StatusCode) 1362 } 1363 1364 tokenResBytes, err := io.ReadAll(res.Body) 1365 h.Must(err) 1366 1367 var jData map[string]interface{} 1368 _ = json.Unmarshal(tokenResBytes, &jData) 1369 1370 if params, ok := jData["form_params"]; ok { 1371 if params != tc.params { 1372 subT.Errorf("%q: expected params %s, got: %s", tc.name, tc.params, params) 1373 } 1374 } else { 1375 if tc.params != "" { 1376 subT.Errorf("%q: expected params %s, got no", tc.name, tc.params) 1377 } 1378 } 1379 if authorization, ok := jData["authorization"]; ok { 1380 if tc.authorization != authorization { 1381 subT.Errorf("%q: expected authorization %s, got: %s", tc.name, tc.authorization, authorization) 1382 } 1383 } else { 1384 if tc.authorization != "" { 1385 subT.Errorf("%q: expected authorization %s, got no", tc.name, tc.authorization) 1386 } 1387 } 1388 1389 message := getFirstAccessLogMessage(hook) 1390 if tc.wantErrLog == "" { 1391 if message != "" { 1392 subT.Errorf("%q: Expected error log: %q, actual: %#v", tc.name, tc.wantErrLog, message) 1393 } 1394 } else { 1395 if !strings.HasPrefix(message, tc.wantErrLog) { 1396 subT.Errorf("%q: Expected error log message: %q, actual: %#v", tc.name, tc.wantErrLog, message) 1397 } 1398 } 1399 }) 1400 } 1401 } 1402 1403 func TestOAuth2_AC_Backend(t *testing.T) { 1404 client := newClient() 1405 helper := test.New(t) 1406 1407 // authorization server creates token response with sub property, JWT ID token with sub claim and userinfo response with sub property from X-Sub request header 1408 asOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1409 sub := req.Header.Get("X-Sub") 1410 if req.URL.Path == "/token" { 1411 if req.Method != http.MethodPost { 1412 rw.WriteHeader(http.StatusMethodNotAllowed) 1413 return 1414 } 1415 rw.Header().Set("Content-Type", "application/json") 1416 rw.WriteHeader(http.StatusOK) 1417 mapClaims := jwt.MapClaims{ 1418 "iss": "https://authorization.server", 1419 "aud": "foo", 1420 "sub": "myself", 1421 "exp": 4000000000, 1422 "iat": 1000, 1423 } 1424 keyBytes, err := os.ReadFile("testdata/integration/files/pkcs8.key") 1425 helper.Must(err) 1426 key, parseErr := jwt.ParseRSAPrivateKeyFromPEM(keyBytes) 1427 helper.Must(parseErr) 1428 idToken, err := lib.CreateJWT("RS256", key, mapClaims, map[string]interface{}{"kid": "rs256"}) 1429 helper.Must(err) 1430 // idToken, _ := lib.CreateJWT("HS256", []byte("$e(rEt"), mapClaims, nil) 1431 idTokenToAdd := `"id_token":"` + idToken + `", 1432 ` 1433 1434 body := []byte(`{ 1435 "access_token": "abcdef0123456789", 1436 ` + idTokenToAdd + 1437 `"sub": "` + sub + `" 1438 }`) 1439 _, werr := rw.Write(body) 1440 helper.Must(werr) 1441 1442 return 1443 } else if req.URL.Path == "/userinfo" { 1444 rw.Header().Set("Content-Type", "application/json") 1445 body := []byte(`{"sub": "` + sub + `"}`) 1446 _, werr := rw.Write(body) 1447 helper.Must(werr) 1448 1449 return 1450 } else if req.URL.Path == "/jwks" { 1451 rw.Header().Set("Content-Type", "application/json") 1452 jsonBytes, rerr := os.ReadFile("testdata/integration/files/jwks.json") 1453 helper.Must(rerr) 1454 b := bytes.NewBuffer(jsonBytes) 1455 _, werr := b.WriteTo(rw) 1456 helper.Must(werr) 1457 1458 return 1459 } else if req.URL.Path == "/.well-known/openid-configuration" { 1460 rw.Header().Set("Content-Type", "application/json") 1461 body := []byte(`{ 1462 "issuer": "https://authorization.server", 1463 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 1464 "token_endpoint": "http://` + req.Host + `/token", 1465 "jwks_uri": "http://` + req.Host + `/jwks", 1466 "userinfo_endpoint": "http://` + req.Host + `/userinfo" 1467 }`) 1468 _, werr := rw.Write(body) 1469 helper.Must(werr) 1470 1471 return 1472 } 1473 rw.WriteHeader(http.StatusBadRequest) 1474 })) 1475 defer asOrigin.Close() 1476 1477 shutdown, hook, err := newCouperWithTemplate("testdata/oauth2/11_couper.hcl", helper, map[string]interface{}{"asOrigin": asOrigin.URL}) 1478 helper.Must(err) 1479 defer shutdown() 1480 1481 type backendExpectation struct { 1482 path, name string 1483 } 1484 1485 type testCase struct { 1486 name string 1487 path string 1488 exp backendExpectation 1489 } 1490 1491 time.Sleep(time.Second * 2) // wait for all oidc/jwks inits 1492 //for _, entry := range hook.AllEntries() { 1493 // println(entry.String()) 1494 //} 1495 //hook.Reset() 1496 1497 for _, tc := range []testCase{ 1498 {"OAuth2 Authorization Code, referenced backend", "/oauth1/redir?code=qeuboub", backendExpectation{"/token", "token"}}, 1499 {"OAuth2 Authorization Code, inline backend", "/oauth2/redir?code=qeuboub", backendExpectation{"/token", "anonymous_56_5_token_endpoint"}}, 1500 {"OIDC Authorization Code, referenced backend", "/oidc1/redir?code=qeuboub", backendExpectation{"/token", "token"}}, 1501 {"OIDC Authorization Code, referenced backends", "/oidc1.1/redir?code=qeuboub", backendExpectation{"/token", "token"}}, 1502 {"OIDC Authorization Code, inline backend", "/oidc2/redir?code=qeuboub", backendExpectation{"/token", "anonymous_98_20_token_backend"}}, 1503 } { 1504 t.Run(tc.name, func(subT *testing.T) { 1505 h := test.New(subT) 1506 1507 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil) 1508 h.Must(err) 1509 1510 req.Header.Set("Cookie", "pkcecv=qerbnr") 1511 1512 hook.Reset() 1513 res, err := client.Do(req) 1514 h.Must(err) 1515 1516 if res.StatusCode != http.StatusOK { 1517 subT.Fatalf("expected Status %d, got: %d", http.StatusOK, res.StatusCode) 1518 } 1519 defer res.Body.Close() 1520 1521 tokenResBytes, err := io.ReadAll(res.Body) 1522 h.Must(err) 1523 1524 var jData map[string]interface{} 1525 h.Must(json.Unmarshal(tokenResBytes, &jData)) 1526 if sub, ok := jData["sub"]; ok { 1527 if sub != "myself" { 1528 subT.Errorf("expected sub %q, got: %q", "myself", sub) 1529 } 1530 } else { 1531 subT.Errorf("expected sub %q, got no", "myself") 1532 } 1533 1534 var seen bool 1535 for _, entry := range hook.AllEntries() { 1536 if entry.Data["type"] == "couper_backend" && entry.Data["backend"] != "" { 1537 if backend, ok := entry.Data["backend"].(string); ok { 1538 if request, ok := entry.Data["request"]; ok { 1539 path, _ := request.(logging.Fields)["path"].(string) 1540 if reflect.DeepEqual(tc.exp, backendExpectation{ 1541 path, backend, 1542 }) { 1543 seen = true 1544 break 1545 } 1546 } 1547 1548 } 1549 } 1550 } 1551 1552 if !seen { 1553 subT.Errorf("expected %#v, got %q", tc.exp, getUpstreamLogBackendName(hook)) 1554 } 1555 }) 1556 } 1557 } 1558 1559 func getBearer(val string) (string, error) { 1560 const bearer = "bearer " 1561 if strings.HasPrefix(strings.ToLower(val), bearer) { 1562 return strings.Trim(val[len(bearer):], " "), nil 1563 } 1564 return "", fmt.Errorf("bearer required with authorization header") 1565 } 1566 1567 func TestOAuth2_CC_Backend(t *testing.T) { 1568 client := newClient() 1569 helper := test.New(t) 1570 1571 // authorization server creates JWT access token with sub-claim from X-Sub request header 1572 asOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1573 sub := req.Header.Get("X-Sub") 1574 if req.URL.Path == "/token" { 1575 rw.Header().Set("Content-Type", "application/json") 1576 rw.WriteHeader(http.StatusOK) 1577 mapClaims := jwt.MapClaims{"sub": sub} 1578 accessToken, _ := lib.CreateJWT("HS256", []byte("$e(rEt"), mapClaims, nil) 1579 body := []byte(`{"access_token": "` + accessToken + `"}`) 1580 _, werr := rw.Write(body) 1581 helper.Must(werr) 1582 1583 return 1584 } 1585 rw.WriteHeader(http.StatusBadRequest) 1586 })) 1587 defer asOrigin.Close() 1588 1589 // resource server sends value of sub claim of JWT bearer token 1590 rsOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1591 authorization := req.Header.Get("Authorization") 1592 tokenString, err := getBearer(authorization) 1593 helper.Must(err) 1594 jwtParser := jwt.NewParser() 1595 claims := jwt.MapClaims{} 1596 _, _, err = jwtParser.ParseUnverified(tokenString, claims) 1597 helper.Must(err) 1598 sub := claims["sub"].(string) 1599 1600 rw.Header().Set("X-Sub2", sub) 1601 rw.WriteHeader(http.StatusNoContent) 1602 })) 1603 defer rsOrigin.Close() 1604 1605 shutdown, hook, err := newCouperWithTemplate("testdata/oauth2/14_couper.hcl", helper, map[string]interface{}{"asOrigin": asOrigin.URL, "rsOrigin": rsOrigin.URL}) 1606 helper.Must(err) 1607 defer shutdown() 1608 1609 type testCase struct { 1610 name string 1611 path string 1612 backendName string 1613 } 1614 1615 for _, tc := range []testCase{ 1616 {"referenced backend", "/rs1", "token"}, 1617 {"inline backend", "/rs2", "anonymous_32_12"}, 1618 } { 1619 t.Run(tc.name, func(subT *testing.T) { 1620 h := test.New(subT) 1621 1622 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil) 1623 h.Must(err) 1624 1625 hook.Reset() 1626 res, err := client.Do(req) 1627 h.Must(err) 1628 1629 if res.StatusCode != http.StatusNoContent { 1630 subT.Errorf("expected Status %d, got: %d", http.StatusNoContent, res.StatusCode) 1631 } 1632 1633 sub := res.Header.Get("X-Sub2") 1634 if sub != "myself" { 1635 subT.Errorf("expected sub %q, got: %q", "myself", sub) 1636 } 1637 1638 backendName := getUpstreamLogBackendName(hook) 1639 if backendName != tc.backendName { 1640 subT.Errorf("expected backend name %q, got: %q", tc.backendName, backendName) 1641 } 1642 }) 1643 } 1644 } 1645 1646 func getUpstreamLogBackendName(hook *logrustest.Hook) string { 1647 for _, entry := range hook.AllEntries() { 1648 if entry.Data["type"] == "couper_backend" && entry.Data["backend"] != "" { 1649 if backend, ok := entry.Data["backend"].(string); ok { 1650 return backend 1651 } 1652 } 1653 } 1654 1655 return "" 1656 } 1657 1658 func TestOAuth2_Locking(t *testing.T) { 1659 helper := test.New(t) 1660 client := test.NewHTTPClient() 1661 1662 token := "token-" 1663 var oauthRequestCount uint32 1664 1665 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1666 if req.URL.Path != "/oauth2" { 1667 rw.WriteHeader(http.StatusBadRequest) 1668 return 1669 } 1670 1671 atomic.AddUint32(&oauthRequestCount, 1) 1672 1673 rw.Header().Set("Content-Type", "application/json") 1674 rw.WriteHeader(http.StatusOK) 1675 1676 n := fmt.Sprintf("%d", atomic.LoadUint32(&oauthRequestCount)) 1677 body := []byte(`{ 1678 "access_token": "` + token + n + `", 1679 "token_type": "bearer", 1680 "expires_in": 1.5 1681 }`) 1682 1683 // Slow down token request 1684 time.Sleep(time.Second) 1685 1686 _, werr := rw.Write(body) 1687 if werr != nil { 1688 t.Error(werr) 1689 } 1690 })) 1691 defer oauthOrigin.Close() 1692 1693 ResourceOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1694 if req.URL.Path == "/resource" { 1695 if auth := req.Header.Get("Authorization"); auth != "" { 1696 rw.Header().Set("Token", auth[len("Bearer "):]) 1697 rw.WriteHeader(http.StatusNoContent) 1698 } 1699 1700 return 1701 } 1702 1703 rw.WriteHeader(http.StatusNotFound) 1704 })) 1705 defer ResourceOrigin.Close() 1706 1707 confPath := "testdata/oauth2/1_retries_couper.hcl" 1708 shutdown, hook, err := newCouperWithTemplate( 1709 confPath, helper, map[string]interface{}{ 1710 "asOrigin": oauthOrigin.URL, 1711 "rsOrigin": ResourceOrigin.URL, 1712 }, 1713 ) 1714 helper.Must(err) 1715 defer shutdown() 1716 1717 req, rerr := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil) 1718 helper.Must(rerr) 1719 1720 hook.Reset() 1721 1722 req.URL.Path = "/" 1723 1724 var responses []*http.Response 1725 var wg sync.WaitGroup 1726 1727 addLock := &sync.Mutex{} 1728 // Fire 5 requests in parallel... 1729 waitCh := make(chan struct{}) 1730 errors := make(chan error, 5) 1731 wg.Add(5) 1732 for i := 0; i < 5; i++ { 1733 go func() { 1734 defer wg.Done() 1735 <-waitCh 1736 res, e := client.Do(req) 1737 if e != nil { 1738 errors <- e 1739 return 1740 } 1741 1742 addLock.Lock() 1743 responses = append(responses, res) 1744 addLock.Unlock() 1745 1746 }() 1747 } 1748 close(waitCh) 1749 wg.Wait() 1750 close(errors) 1751 for err := range errors { 1752 if err != nil { 1753 t.Error(err) 1754 } 1755 } 1756 1757 for _, res := range responses { 1758 if res.StatusCode != http.StatusNoContent { 1759 t.Errorf("Expected status NoContent, got: %d", res.StatusCode) 1760 } 1761 1762 if token+"1" != res.Header.Get("Token") { 1763 t.Errorf("Invalid token given: want %s1, got: %s", token, res.Header.Get("Token")) 1764 } 1765 } 1766 1767 if count := atomic.LoadUint32(&oauthRequestCount); count != 1 { 1768 t.Errorf("OAuth2 requests: want 1, got: %d", count) 1769 } 1770 1771 t.Run("Lock is effective", func(subT *testing.T) { 1772 // Wait until token has expired. 1773 time.Sleep(2 * time.Second) 1774 1775 // Fetch new token. 1776 go func() { 1777 res, err := client.Do(req) 1778 if err != nil { 1779 subT.Error(err) 1780 return 1781 } 1782 1783 if token+"2" != res.Header.Get("Token") { 1784 subT.Errorf("Received wrong token: want %s2, got: %s", token, res.Header.Get("Token")) 1785 } 1786 }() 1787 1788 // Slow response due to lock 1789 start := time.Now() 1790 res, err := client.Do(req) 1791 if err != nil { 1792 subT.Error(err) 1793 return 1794 } 1795 1796 timeElapsed := time.Since(start) 1797 1798 if token+"2" != res.Header.Get("Token") { 1799 subT.Errorf("Received wrong token: want %s2, got: %s", token, res.Header.Get("Token")) 1800 } 1801 1802 if timeElapsed < time.Second { 1803 subT.Errorf("Response came too fast: dysfunctional lock?! (%s)", timeElapsed.String()) 1804 } 1805 }) 1806 1807 t.Run("Mem store expiry", func(subT *testing.T) { 1808 // Wait again until token has expired. 1809 time.Sleep(2 * time.Second) 1810 h := test.New(subT) 1811 // Request fresh token and store in memstore 1812 res, err := client.Do(req) 1813 h.Must(err) 1814 1815 if res.StatusCode != http.StatusNoContent { 1816 subT.Errorf("Unexpected response status: want %d, got: %d", http.StatusNoContent, res.StatusCode) 1817 } 1818 1819 if token+"3" != res.Header.Get("Token") { 1820 subT.Errorf("Received wrong token: want %s3, got: %s", token, res.Header.Get("Token")) 1821 } 1822 1823 if count := atomic.LoadUint32(&oauthRequestCount); count != 3 { 1824 subT.Errorf("Unexpected number of OAuth2 requests: want 3, got: %d", count) 1825 } 1826 1827 // Disconnect OAuth server 1828 oauthOrigin.Close() 1829 1830 // Next request gets token from memstore 1831 res, err = client.Do(req) 1832 h.Must(err) 1833 if res.StatusCode != http.StatusNoContent { 1834 subT.Errorf("Unexpected response status: want %d, got: %d", http.StatusNoContent, res.StatusCode) 1835 } 1836 1837 if token+"3" != res.Header.Get("Token") { 1838 subT.Errorf("Wrong token from mem store: want %s3, got: %s", token, res.Header.Get("Token")) 1839 } 1840 1841 // Wait until token has expired. Next request accesses the OAuth server again. 1842 time.Sleep(2 * time.Second) 1843 res, err = newClient().Do(req) 1844 h.Must(err) 1845 if res.StatusCode != http.StatusBadGateway { 1846 subT.Errorf("Unexpected response status: want %d, got: %d", http.StatusBadGateway, res.StatusCode) 1847 } 1848 }) 1849 } 1850 1851 func TestNestedBackendOauth2(t *testing.T) { 1852 helper := test.New(t) 1853 shutdown, hook := newCouperMultiFiles("testdata/oauth2/15_couper.hcl", "", helper) 1854 defer shutdown() 1855 1856 time.Sleep(time.Second / 2) 1857 1858 logs := hook.AllEntries() 1859 for _, log := range logs { 1860 if log.Level == logrus.ErrorLevel { 1861 t.Error(log.String()) 1862 } 1863 } 1864 } 1865 1866 func TestTokenRequest(t *testing.T) { 1867 helper := test.New(t) 1868 1869 asOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1870 if req.Header.Get("KeyId") != "the-key" { 1871 rw.WriteHeader(http.StatusUnauthorized) 1872 return 1873 } 1874 1875 if user, _, _ := req.BasicAuth(); user != "the-key" { 1876 rw.WriteHeader(http.StatusUnauthorized) 1877 return 1878 } 1879 1880 reqBody, _ := io.ReadAll(req.Body) 1881 1882 // path_prefix context test prepends "the-key" 1883 1884 if req.URL.Path == "/the-key/token" { 1885 expBody := "grant_type=client_credentials" 1886 if expBody != string(reqBody) { 1887 t.Errorf("wrong request body /token\nwant: %q\ngot: %q", expBody, reqBody) 1888 } 1889 rw.Header().Set("Content-Type", "application/json") 1890 rw.WriteHeader(http.StatusOK) 1891 1892 body := []byte(`{ 1893 "access_token": "tok0", 1894 "token_type": "bearer", 1895 "expires_in": 100 1896 }`) 1897 _, werr := rw.Write(body) 1898 helper.Must(werr) 1899 1900 return 1901 } else if req.URL.Path == "/the-key/token1" { 1902 expBody := "client_id=clid&client_secret=cls&grant_type=client_credentials" 1903 if expBody != string(reqBody) { 1904 t.Errorf("wrong request body /token1\nwant: %q\ngot: %q", expBody, reqBody) 1905 } 1906 rw.Header().Set("Content-Type", "application/json") 1907 rw.WriteHeader(http.StatusOK) 1908 1909 body := []byte(`{ 1910 "access_token": "tok1", 1911 "token_type": "bearer", 1912 "expires_in": 100 1913 }`) 1914 _, werr := rw.Write(body) 1915 helper.Must(werr) 1916 1917 return 1918 } else if req.URL.Path == "/the-key/token2" { 1919 if req.URL.RawQuery != "foo=bar" { 1920 t.Errorf("wrong request URL query /token2\nwant: %q\ngot: %q", "foo=bar", req.URL.RawQuery) 1921 } 1922 expBody := "client_id=clid&client_secret=cls&grant_type=password&password=asdf&username=user" 1923 if expBody != string(reqBody) { 1924 t.Errorf("wrong request body /token2\nwant: %q\ngot: %q", expBody, reqBody) 1925 } 1926 rw.Header().Set("Content-Type", "application/json") 1927 rw.WriteHeader(http.StatusOK) 1928 1929 body := []byte(`{ 1930 "access_token": "tok2", 1931 "token_type": "bearer", 1932 "expires_in": 100 1933 }`) 1934 _, werr := rw.Write(body) 1935 helper.Must(werr) 1936 1937 return 1938 } 1939 rw.WriteHeader(http.StatusBadRequest) 1940 })) 1941 defer asOrigin.Close() 1942 1943 rsOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1944 if req.Header.Get("Authorization") != "Bearer tok0" || 1945 req.Header.Get("Auth-1") != "tok1" || 1946 req.Header.Get("Auth-2") != "tok2" || 1947 req.Header.Get("Auth-3") != "tok2" || 1948 req.Header.Get("Auth-4") != "tok1" || 1949 req.Header.Get("Auth-5") != "tok2" || 1950 req.Header.Get("Auth-6") != "tok2" || 1951 req.Header.Get("KeyId") != "the-key" { 1952 rw.WriteHeader(http.StatusUnauthorized) 1953 return 1954 } 1955 1956 if req.URL.Path == "/resource" { 1957 rw.WriteHeader(http.StatusNoContent) 1958 return 1959 } 1960 1961 rw.WriteHeader(http.StatusNotFound) 1962 })) 1963 defer rsOrigin.Close() 1964 1965 vaultOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 1966 if req.URL.Path == "/key" { 1967 rw.WriteHeader(http.StatusOK) 1968 1969 body := []byte("the-key") 1970 _, werr := rw.Write(body) 1971 helper.Must(werr) 1972 1973 return 1974 } 1975 rw.WriteHeader(http.StatusBadRequest) 1976 })) 1977 defer vaultOrigin.Close() 1978 1979 confPath := "testdata/oauth2/token_request.hcl" 1980 shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"asOrigin": asOrigin.URL, "rsOrigin": rsOrigin.URL, "vaultOrigin": vaultOrigin.URL}) 1981 helper.Must(err) 1982 defer shutdown() 1983 1984 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/resource", nil) 1985 helper.Must(err) 1986 hook.Reset() 1987 res, err := newClient().Do(req) 1988 helper.Must(err) 1989 1990 if res.StatusCode != http.StatusNoContent { 1991 t.Errorf("expected status %d, got: %d", http.StatusNoContent, res.StatusCode) 1992 } 1993 } 1994 1995 func TestTokenRequest_Config_Errors(t *testing.T) { 1996 type testCase struct { 1997 name string 1998 hcl string 1999 error string 2000 } 2001 2002 for _, tc := range []testCase{ 2003 { 2004 "invalid label", 2005 `server {} 2006 definitions { 2007 backend "be" { 2008 beta_token_request "the label" { 2009 url = "http://localhost:8082/token2" 2010 token = beta_token_response.json_body.tok 2011 ttl = "1m" 2012 } 2013 } 2014 } 2015 `, 2016 "couper.hcl:4,24-35: label contains invalid character(s), allowed are 'a-z', 'A-Z', '0-9' and '_';", 2017 }, 2018 { 2019 "multiple default labels (LabelRanges)", 2020 `server {} 2021 definitions { 2022 backend "be" { 2023 beta_token_request { 2024 url = "http://localhost:8081/token1" 2025 token = beta_token_response.json_body.tok 2026 ttl = "1m" 2027 } 2028 beta_token_request "default" { 2029 url = "http://localhost:8082/token2" 2030 token = beta_token_response.json_body.tok 2031 ttl = "2m" 2032 } 2033 } 2034 } 2035 `, 2036 "couper.hcl:9,24-33: token request names (either default or explicitly set via label) must be unique: \"default\";", 2037 }, 2038 { 2039 "multiple default labels (DefRange)", 2040 `server {} 2041 definitions { 2042 backend "be" { 2043 beta_token_request "default" { 2044 url = "http://localhost:8081/token1" 2045 token = beta_token_response.json_body.tok 2046 ttl = "1m" 2047 } 2048 beta_token_request { 2049 url = "http://localhost:8082/token2" 2050 token = beta_token_response.json_body.tok 2051 ttl = "2m" 2052 } 2053 } 2054 } 2055 `, 2056 "couper.hcl:9,5-23: token request names (either default or explicitly set via label) must be unique: \"default\";", 2057 }, 2058 { 2059 "multiple default labels (inline backend)", 2060 ` 2061 server { 2062 endpoint "/" { 2063 proxy { 2064 backend { 2065 beta_token_request "default" { 2066 url = "http://localhost:8081/token1" 2067 token = beta_token_response.json_body.tok 2068 ttl = "1m" 2069 } 2070 beta_token_request { 2071 url = "http://localhost:8082/token2" 2072 token = beta_token_response.json_body.tok 2073 ttl = "2m" 2074 } 2075 } 2076 } 2077 } 2078 } 2079 `, 2080 "couper.hcl:11,9-27: token request names (either default or explicitly set via label) must be unique: \"default\";", 2081 }, 2082 { 2083 "multiple labels", 2084 `server {} 2085 definitions { 2086 backend "be" { 2087 beta_token_request "a" { 2088 url = "http://localhost:8081/token1" 2089 token = beta_token_response.json_body.tok 2090 ttl = "1m" 2091 } 2092 beta_token_request "a" { 2093 url = "http://localhost:8082/token2" 2094 token = beta_token_response.json_body.tok 2095 ttl = "2m" 2096 } 2097 } 2098 } 2099 `, 2100 "couper.hcl:9,24-27: token request names (either default or explicitly set via label) must be unique: \"a\"; ", 2101 }, 2102 { 2103 "multiple labels (inline backend)", 2104 ` 2105 server { 2106 endpoint "/" { 2107 proxy { 2108 backend { 2109 beta_token_request "a" { 2110 url = "http://localhost:8081/token1" 2111 token = beta_token_response.json_body.tok 2112 ttl = "1m" 2113 } 2114 beta_token_request "a" { 2115 url = "http://localhost:8082/token2" 2116 token = beta_token_response.json_body.tok 2117 ttl = "2m" 2118 } 2119 } 2120 } 2121 } 2122 } 2123 `, 2124 "couper.hcl:11,28-31: token request names (either default or explicitly set via label) must be unique: \"a\"; ", 2125 }, 2126 } { 2127 var errMsg string 2128 _, err := configload.LoadBytes([]byte(tc.hcl), "couper.hcl") 2129 if err != nil { 2130 if _, ok := err.(errors.GoError); ok { 2131 errMsg = err.(errors.GoError).LogError() 2132 } else { 2133 errMsg = err.Error() 2134 } 2135 } 2136 2137 if !strings.HasPrefix(errMsg, tc.error) { 2138 t.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot: %q", tc.name, tc.error, errMsg) 2139 } 2140 } 2141 } 2142 2143 func TestTokenRequest_Runtime_Errors(t *testing.T) { 2144 helper := test.New(t) 2145 2146 asOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 2147 if req.URL.Path == "/token" { 2148 rw.Header().Set("Content-Type", "application/json") 2149 rw.WriteHeader(http.StatusOK) 2150 2151 body := []byte(`{ 2152 "access_token": "abcdef0123456789", 2153 "token_type": "bearer", 2154 "expires_in": 100 2155 }`) 2156 _, werr := rw.Write(body) 2157 helper.Must(werr) 2158 return 2159 } 2160 rw.WriteHeader(http.StatusBadRequest) 2161 })) 2162 defer asOrigin.Close() 2163 2164 type testCase struct { 2165 name string 2166 filename string 2167 wantStatus int 2168 wantErrLog string 2169 } 2170 2171 for _, tc := range []testCase{ 2172 {"token request error, handled by error handler", "01_token_request_error.hcl", http.StatusNoContent, "backend error: be: request error: tr: token request failed"}, 2173 {"token expression evaluation error", "02_token_request_error.hcl", http.StatusBadGateway, "couper-bytes.hcl:23,15-31: Call to unknown function; There is no function named \"evaluation_error\"."}, 2174 {"null token", "03_token_request_error.hcl", http.StatusBadGateway, "backend error: be: request error: tr: token expression evaluates to null"}, 2175 {"non-string token", "04_token_request_error.hcl", http.StatusBadGateway, "backend error: be: request error: tr: token expression must evaluate to a string"}, 2176 {"ttl expression evaluation error", "05_token_request_error.hcl", http.StatusBadGateway, "couper-bytes.hcl:24,13-29: Call to unknown function; There is no function named \"evaluation_error\"."}, 2177 {"null ttl", "06_token_request_error.hcl", http.StatusBadGateway, "backend error: be: request error: tr: ttl expression evaluates to null"}, 2178 {"non-string ttl", "07_token_request_error.hcl", http.StatusBadGateway, "backend error: be: request error: tr: ttl expression must evaluate to a string"}, 2179 {"non-duration ttl", "08_token_request_error.hcl", http.StatusBadGateway, "backend error: be: request error: tr: ttl: time: invalid duration \"no duration\""}, 2180 } { 2181 t.Run(tc.name, func(subT *testing.T) { 2182 h := test.New(subT) 2183 2184 shutdown, hook, err := newCouperWithTemplate("testdata/oauth2/"+tc.filename, h, map[string]interface{}{"asOrigin": asOrigin.URL}) 2185 h.Must(err) 2186 defer shutdown() 2187 2188 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/resource", nil) 2189 h.Must(err) 2190 hook.Reset() 2191 res, err := newClient().Do(req) 2192 h.Must(err) 2193 2194 if res.StatusCode != tc.wantStatus { 2195 subT.Errorf("expected status %d, got: %d", tc.wantStatus, res.StatusCode) 2196 } 2197 2198 message := getFirstAccessLogMessage(hook) 2199 if message != tc.wantErrLog { 2200 subT.Errorf("error log\nwant: %q\ngot: %q", tc.wantErrLog, message) 2201 } 2202 2203 shutdown() 2204 }) 2205 } 2206 2207 asOrigin.Close() 2208 }