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  }