github.com/avenga/couper@v1.12.2/eval/lib/oauth2_test.go (about)

     1  package lib_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"testing"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/avenga/couper/accesscontrol/jwk"
    14  	"github.com/avenga/couper/cache"
    15  	"github.com/avenga/couper/config"
    16  	"github.com/avenga/couper/config/configload"
    17  	"github.com/avenga/couper/config/request"
    18  	"github.com/avenga/couper/config/runtime"
    19  	"github.com/avenga/couper/eval"
    20  	"github.com/avenga/couper/eval/lib"
    21  	"github.com/avenga/couper/internal/seetie"
    22  	"github.com/avenga/couper/internal/test"
    23  	"github.com/avenga/couper/oauth2/oidc"
    24  )
    25  
    26  func TestNewOAuthAuthorizationURLFunction(t *testing.T) {
    27  	helper := test.New(t)
    28  
    29  	var origin *httptest.Server
    30  	origin = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
    31  		var conf interface{}
    32  		if req.URL.Path == "/.well-known/openid-configuration" {
    33  			conf = &oidc.OpenidConfiguration{
    34  				AuthorizationEndpoint:         origin.URL + "/auth",
    35  				CodeChallengeMethodsSupported: []string{config.CcmS256},
    36  				Issuer:                        "thatsme",
    37  				JwksURI:                       origin.URL + "/jwks",
    38  				TokenEndpoint:                 origin.URL + "/token",
    39  				UserinfoEndpoint:              origin.URL + "/userinfo",
    40  			}
    41  		} else if req.URL.Path == "/jwks" {
    42  			conf = jwk.JWKSData{}
    43  		}
    44  
    45  		b, err := json.Marshal(conf)
    46  		helper.Must(err)
    47  		_, err = rw.Write(b)
    48  		helper.Must(err)
    49  	}))
    50  	defer origin.Close()
    51  
    52  	confURL := origin.URL + "/.well-known/openid-configuration"
    53  	authURL := origin.URL + "/auth"
    54  	tokenURL := origin.URL + "/token"
    55  
    56  	log, _ := test.NewLogger()
    57  	logger := log.WithContext(context.Background())
    58  
    59  	type testCase struct {
    60  		name      string
    61  		config    string
    62  		wantRedir string
    63  		wantScope string
    64  		wantNonce bool
    65  		wantState bool
    66  		wantPKCE  bool
    67  	}
    68  
    69  	for _, tc := range []testCase{
    70  		{
    71  			"oidc",
    72  			`server {}
    73  definitions {
    74    oidc "auth-ref" {
    75      client_id = "test-id"
    76      client_secret = "test-s3cr3t"
    77      configuration_url = "` + confURL + `"
    78      redirect_uri = "/cb"
    79      verifier_value = "asdf"
    80    }
    81  }
    82  `,
    83  			"https://couper.io/cb",
    84  			"openid",
    85  			true,
    86  			false,
    87  			false,
    88  		},
    89  		{
    90  			"oidc, redir URI from env var and using function",
    91  			`server {}
    92  definitions {
    93    oidc "auth-ref" {
    94      client_id = "test-id"
    95      client_secret = "test-s3cr3t"
    96      configuration_url = "` + confURL + `"
    97      redirect_uri = split(" ", env.REDIR_URIS)[0]
    98      verifier_value = "asdf"
    99    }
   100  }
   101  defaults {
   102    environment_variables = {
   103      REDIR_URIS = "/cb /cb2"
   104    }
   105  }
   106  `,
   107  			"https://couper.io/cb",
   108  			"openid",
   109  			true,
   110  			false,
   111  			false,
   112  		},
   113  		{
   114  			"oidc, absolute redir URI",
   115  			`server {}
   116  definitions {
   117    oidc "auth-ref" {
   118      client_id = "test-id"
   119      client_secret = "test-s3cr3t"
   120      configuration_url = "` + confURL + `"
   121      redirect_uri = "https://example.com/cb"
   122      verifier_value = "asdf"
   123    }
   124  }
   125  `,
   126  			"https://example.com/cb",
   127  			"openid",
   128  			true,
   129  			false,
   130  			false,
   131  		},
   132  		{
   133  			"oidc, empty scope",
   134  			`server {}
   135  definitions {
   136    oidc "auth-ref" {
   137      client_id = "test-id"
   138      client_secret = "test-s3cr3t"
   139      configuration_url = "` + confURL + `"
   140      redirect_uri = "/cb"
   141      verifier_value = "asdf"
   142      scope = ""
   143    }
   144  }
   145  `,
   146  			"https://couper.io/cb",
   147  			"openid",
   148  			true,
   149  			false,
   150  			false,
   151  		},
   152  		{
   153  			"oidc, email scope",
   154  			`server {}
   155  definitions {
   156    oidc "auth-ref" {
   157      client_id = "test-id"
   158      client_secret = "test-s3cr3t"
   159      configuration_url = "` + confURL + `"
   160      redirect_uri = "/cb"
   161      verifier_value = "asdf"
   162      scope = "email"
   163    }
   164  }
   165  `,
   166  			"https://couper.io/cb",
   167  			"openid email",
   168  			true,
   169  			false,
   170  			false,
   171  		},
   172  		{
   173  			"oidc, verifier_method ccm_s256",
   174  			`server {}
   175  definitions {
   176    oidc "auth-ref" {
   177      client_id = "test-id"
   178      client_secret = "test-s3cr3t"
   179      configuration_url = "` + confURL + `"
   180      redirect_uri = "/cb"
   181      verifier_method = "ccm_s256"
   182      verifier_value = "asdf"
   183    }
   184  }
   185  `,
   186  			"https://couper.io/cb",
   187  			"openid",
   188  			false,
   189  			false,
   190  			true,
   191  		},
   192  		{
   193  			"oauth2 authorization code",
   194  			`server {}
   195  definitions {
   196    beta_oauth2 "auth-ref" {
   197      grant_type = "authorization_code"
   198      client_id = "test-id"
   199      client_secret = "test-s3cr3t"
   200      authorization_endpoint = "` + authURL + `"
   201      token_endpoint = "` + tokenURL + `"
   202      redirect_uri = "/cb"
   203      verifier_method = "state"
   204      verifier_value = "asdf"
   205    }
   206  }
   207  `,
   208  			"https://couper.io/cb",
   209  			"",
   210  			false,
   211  			true,
   212  			false,
   213  		},
   214  		{
   215  			"oauth2 authorization code, redir URI from env var",
   216  			`server {}
   217  definitions {
   218    beta_oauth2 "auth-ref" {
   219      grant_type = "authorization_code"
   220      client_id = "test-id"
   221      client_secret = "test-s3cr3t"
   222      authorization_endpoint = "` + authURL + `"
   223      token_endpoint = "` + tokenURL + `"
   224      redirect_uri = env.REDIR_URI
   225      verifier_method = "state"
   226      verifier_value = "asdf"
   227    }
   228  }
   229  defaults {
   230    environment_variables = {
   231      REDIR_URI = "/cb"
   232    }
   233  }
   234  `,
   235  			"https://couper.io/cb",
   236  			"",
   237  			false,
   238  			true,
   239  			false,
   240  		},
   241  		{
   242  			"oauth2 authorization code, absolute redir URI",
   243  			`server {}
   244  definitions {
   245    beta_oauth2 "auth-ref" {
   246      grant_type = "authorization_code"
   247      client_id = "test-id"
   248      client_secret = "test-s3cr3t"
   249      authorization_endpoint = "` + authURL + `"
   250      token_endpoint = "` + tokenURL + `"
   251      redirect_uri = "https://example.com/cb"
   252      verifier_method = "state"
   253      verifier_value = "asdf"
   254    }
   255  }
   256  `,
   257  			"https://example.com/cb",
   258  			"",
   259  			false,
   260  			true,
   261  			false,
   262  		},
   263  		{
   264  			"oauth2 authorization code, empty scope",
   265  			`server {}
   266  definitions {
   267    beta_oauth2 "auth-ref" {
   268      grant_type = "authorization_code"
   269      client_id = "test-id"
   270      client_secret = "test-s3cr3t"
   271      authorization_endpoint = "` + authURL + `"
   272      token_endpoint = "` + tokenURL + `"
   273      redirect_uri = "/cb"
   274      verifier_method = "state"
   275      verifier_value = "asdf"
   276      scope = ""
   277    }
   278  }
   279  `,
   280  			"https://couper.io/cb",
   281  			"",
   282  			false,
   283  			true,
   284  			false,
   285  		},
   286  		{
   287  			"oauth2 authorization code, 'foo bar' scope",
   288  			`server {}
   289  definitions {
   290    beta_oauth2 "auth-ref" {
   291      grant_type = "authorization_code"
   292      client_id = "test-id"
   293      client_secret = "test-s3cr3t"
   294      authorization_endpoint = "` + authURL + `"
   295      token_endpoint = "` + tokenURL + `"
   296      redirect_uri = "/cb"
   297      verifier_method = "state"
   298      verifier_value = "asdf"
   299      scope = "foo bar"
   300    }
   301  }
   302  `,
   303  			"https://couper.io/cb",
   304  			"foo bar",
   305  			false,
   306  			true,
   307  			false,
   308  		},
   309  		{
   310  			"oauth2 authorization code, verifier_method ccm_s256",
   311  			`server {}
   312  definitions {
   313    beta_oauth2 "auth-ref" {
   314      grant_type = "authorization_code"
   315      client_id = "test-id"
   316      client_secret = "test-s3cr3t"
   317      authorization_endpoint = "` + authURL + `"
   318      token_endpoint = "` + tokenURL + `"
   319      redirect_uri = "/cb"
   320      verifier_method = "ccm_s256"
   321      verifier_value = "asdf"
   322    }
   323  }
   324  `,
   325  			"https://couper.io/cb",
   326  			"",
   327  			false,
   328  			false,
   329  			true,
   330  		},
   331  	} {
   332  		t.Run(tc.name, func(subT *testing.T) {
   333  			h := test.New(subT)
   334  
   335  			couperConf, err := configload.LoadBytes([]byte(tc.config), "test.hcl")
   336  			h.Must(err)
   337  
   338  			quitCh := make(chan struct{}, 1)
   339  			defer close(quitCh)
   340  			memStore := cache.New(logger, quitCh)
   341  
   342  			ctx, cancel := context.WithCancel(couperConf.Context)
   343  			couperConf.Context = ctx
   344  			defer cancel()
   345  
   346  			_, err = runtime.NewServerConfiguration(couperConf, logger, memStore)
   347  			helper.Must(err)
   348  
   349  			req, rerr := http.NewRequest(http.MethodGet, "https://couper.io/", nil)
   350  			helper.Must(rerr)
   351  			req = req.Clone(context.Background())
   352  
   353  			hclCtx := couperConf.Context.(*eval.Context).
   354  				WithClientRequest(req).
   355  				HCLContext()
   356  
   357  			val, furr := hclCtx.Functions[lib.FnOAuthAuthorizationURL].Call([]cty.Value{cty.StringVal("auth-ref")})
   358  			helper.Must(furr)
   359  
   360  			authURL := seetie.ValueToString(val)
   361  			authURLObj, perr := url.Parse(authURL)
   362  			helper.Must(perr)
   363  
   364  			query := authURLObj.Query()
   365  
   366  			if rt := query.Get("response_type"); rt != "code" {
   367  				subT.Errorf("response_type want: %v; got: %v", "code", rt)
   368  			}
   369  
   370  			if clID := query.Get("client_id"); clID != "test-id" {
   371  				subT.Errorf("client_id want: %v; got: %v", "test-id", clID)
   372  			}
   373  
   374  			if redir := query.Get("redirect_uri"); redir != tc.wantRedir {
   375  				subT.Errorf("redirect_uri want: %v; got: %v", tc.wantRedir, redir)
   376  			}
   377  
   378  			if tc.wantScope == "" && query.Has("scope") {
   379  				subT.Error("scope not expected")
   380  			}
   381  			if scope := query.Get("scope"); scope != tc.wantScope {
   382  				subT.Errorf("scope want: %v; got: %v", tc.wantScope, scope)
   383  			}
   384  
   385  			nonce := query.Get("nonce")
   386  			if tc.wantNonce {
   387  				if nonce == "" {
   388  					subT.Error("missing nonce")
   389  				}
   390  			} else {
   391  				if nonce != "" {
   392  					subT.Error("nonce not expected")
   393  				}
   394  			}
   395  
   396  			state := query.Get("state")
   397  			if tc.wantState {
   398  				if state == "" {
   399  					subT.Error("missing state")
   400  				}
   401  			} else {
   402  				if state != "" {
   403  					subT.Error("state not expected")
   404  				}
   405  			}
   406  
   407  			codeChallenge := query.Get("code_challenge")
   408  			codeChallengeMethod := query.Get("code_challenge_method")
   409  			if tc.wantPKCE {
   410  				if codeChallenge == "" {
   411  					subT.Error("missing code_challenge")
   412  				}
   413  				if codeChallengeMethod != "S256" {
   414  					subT.Errorf("code_challenge want: %v; got: %v\n", "S256", codeChallengeMethod)
   415  				}
   416  			} else {
   417  				if codeChallenge != "" {
   418  					subT.Error("code_challenge not expected")
   419  				}
   420  			}
   421  		})
   422  	}
   423  }
   424  
   425  func TestOAuthAuthorizationURLError(t *testing.T) {
   426  	tests := []struct {
   427  		name    string
   428  		config  string
   429  		label   string
   430  		wantErr string
   431  	}{
   432  		{
   433  			"missing oidc/beta_oauth2 definitions",
   434  			`
   435  			server {}
   436  			definitions {
   437  			}
   438  			`,
   439  			"MyLabel",
   440  			`missing oidc or beta_oauth2 block with referenced label "MyLabel"`,
   441  		},
   442  		{
   443  			"missing referenced oidc/beta_oauth2",
   444  			`
   445  			server {}
   446  			definitions {
   447  			  beta_oauth2 "auth-ref" {
   448  			    grant_type = "authorization_code"
   449  			    client_id = "test-id"
   450  			    client_secret = "test-s3cr3t"
   451  			    authorization_endpoint = "https://a.s./auth"
   452  			    token_endpoint = "https://a.s./token"
   453  			    redirect_uri = "/cb"
   454  			    verifier_method = "ccm_s256"
   455  			    verifier_value = "asdf"
   456  			  }
   457  			}
   458  			`,
   459  			"MyLabel",
   460  			`missing oidc or beta_oauth2 block with referenced label "MyLabel"`,
   461  		},
   462  	}
   463  
   464  	for _, tt := range tests {
   465  		t.Run(tt.name, func(subT *testing.T) {
   466  			h := test.New(subT)
   467  			couperConf, err := configload.LoadBytes([]byte(tt.config), "test.hcl")
   468  			h.Must(err)
   469  
   470  			ctx, cancel := context.WithCancel(couperConf.Context)
   471  			couperConf.Context = ctx
   472  			defer cancel()
   473  
   474  			evalContext := couperConf.Context.Value(request.ContextType).(*eval.Context)
   475  			req, err := http.NewRequest(http.MethodGet, "https://www.example.com/foo", nil)
   476  			h.Must(err)
   477  			evalContext = evalContext.WithClientRequest(req)
   478  
   479  			_, err = evalContext.HCLContext().Functions[lib.FnOAuthAuthorizationURL].Call([]cty.Value{cty.StringVal(tt.label)})
   480  			if err == nil {
   481  				subT.Error("expected an error, got nothing")
   482  				return
   483  			}
   484  			if err.Error() != tt.wantErr {
   485  				subT.Errorf("\nWant:\t%q\nGot:\t%q", tt.wantErr, err.Error())
   486  			}
   487  		})
   488  	}
   489  }