git.iko.soy/typeable/oauth2-proxy
on master help sign in
typeable/oauth2-proxy/oauthproxy_test.go
3810 lines 101 kB history raw
1package main
3import (
4 "context"
5 "crypto"
6 "encoding/base64"
7 "fmt"
8 "io"
9 "net/http"
10 "net/http/httptest"
11 "net/url"
12 "regexp"
13 "strings"
14 "testing"
15 "time"
17 "github.com/coreos/go-oidc/v3/oidc"
18 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
24 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"
25 sessionscookie "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
28 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
29 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
30 "github.com/stretchr/testify/assert"
31 "github.com/stretchr/testify/require"
34const (
35 // The rawCookieSecret is 32 bytes and the base64CookieSecret is the base64
36 // encoded version of this.
37 rawCookieSecret = "secretthirtytwobytes+abcdefghijk"
38 base64CookieSecret = "c2VjcmV0dGhpcnR5dHdvYnl0ZXMrYWJjZGVmZ2hpams"
39 clientID = "3984n253984d7348dm8234yf982t"
40 clientSecret = "gv3498mfc9t23y23974dm2394dm9"
43func init() {
44 logger.SetFlags(logger.Lshortfile)
47func TestRobotsTxt(t *testing.T) {
48 opts := baseTestOptions()
49 err := validation.Validate(opts)
50 assert.NoError(t, err)
52 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
53 if err != nil {
54 t.Fatal(err)
55 }
56 rw := httptest.NewRecorder()
57 req, _ := http.NewRequest(http.MethodGet, "/robots.txt", nil)
58 proxy.ServeHTTP(rw, req)
59 assert.Equal(t, 200, rw.Code)
60 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
63type TestProvider struct {
64 *providers.ProviderData
65 EmailAddress string
66 ValidToken bool
67 GroupValidator func(string) bool
70var _ providers.Provider = (*TestProvider)(nil)
72func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
73 return &TestProvider{
74 ProviderData: &providers.ProviderData{
75 ProviderName: "Test Provider",
76 LoginURL: &url.URL{
77 Scheme: "http",
78 Host: providerURL.Host,
79 Path: "/oauth/authorize",
80 },
81 RedeemURL: &url.URL{
82 Scheme: "http",
83 Host: providerURL.Host,
84 Path: "/oauth/token",
85 },
86 ProfileURL: &url.URL{
87 Scheme: "http",
88 Host: providerURL.Host,
89 Path: "/api/v1/profile",
90 },
91 Scope: "profile.email",
92 },
93 EmailAddress: emailAddress,
94 GroupValidator: func(s string) bool {
95 return true
96 },
97 }
100func (tp *TestProvider) GetEmailAddress(_ context.Context, _ *sessions.SessionState) (string, error) {
101 return tp.EmailAddress, nil
104func (tp *TestProvider) ValidateSession(_ context.Context, _ *sessions.SessionState) bool {
105 return tp.ValidToken
108func Test_redeemCode(t *testing.T) {
109 opts := baseTestOptions()
110 err := validation.Validate(opts)
111 assert.NoError(t, err)
113 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
114 if err != nil {
115 t.Fatal(err)
116 }
118 req := httptest.NewRequest(http.MethodGet, "/", nil)
119 _, err = proxy.redeemCode(req, "")
120 assert.Equal(t, providers.ErrMissingCode, err)
123func Test_enrichSession(t *testing.T) {
124 const (
125 sessionUser = "Mr Session"
126 sessionEmail = "session@example.com"
127 providerEmail = "provider@example.com"
128 )
130 testCases := map[string]struct {
131 session *sessions.SessionState
132 expectedUser string
133 expectedEmail string
134 }{
135 "Session already has enrichable fields": {
136 session: &sessions.SessionState{
137 User: sessionUser,
138 Email: sessionEmail,
139 },
140 expectedUser: sessionUser,
141 expectedEmail: sessionEmail,
142 },
143 "Session is missing Email and GetEmailAddress is implemented": {
144 session: &sessions.SessionState{
145 User: sessionUser,
146 },
147 expectedUser: sessionUser,
148 expectedEmail: providerEmail,
149 },
150 "Session is missing User and GetUserName is not implemented": {
151 session: &sessions.SessionState{
152 Email: sessionEmail,
153 },
154 expectedUser: "",
155 expectedEmail: sessionEmail,
156 },
157 }
159 for name, tc := range testCases {
160 t.Run(name, func(t *testing.T) {
161 opts := baseTestOptions()
162 err := validation.Validate(opts)
163 assert.NoError(t, err)
165 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
166 if err != nil {
167 t.Fatal(err)
168 }
169 proxy.provider = NewTestProvider(&url.URL{Host: "www.example.com"}, providerEmail)
171 err = proxy.enrichSessionState(context.Background(), tc.session)
172 assert.NoError(t, err)
173 assert.Equal(t, tc.expectedUser, tc.session.User)
174 assert.Equal(t, tc.expectedEmail, tc.session.Email)
175 })
176 }
179func TestBasicAuthPassword(t *testing.T) {
180 providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
181 logger.Printf("%#v", r)
182 var payload string
183 switch r.URL.Path {
184 case "/oauth/token":
185 payload = `{"access_token": "my_auth_token"}`
186 default:
187 payload = r.Header.Get("Authorization")
188 if payload == "" {
189 payload = "No Authorization header found."
190 }
191 }
192 w.WriteHeader(200)
193 _, err := w.Write([]byte(payload))
194 if err != nil {
195 t.Fatal(err)
196 }
197 }))
199 basicAuthPassword := "This is a secure password"
200 opts := baseTestOptions()
201 opts.UpstreamServers = options.UpstreamConfig{
202 Upstreams: []options.Upstream{
203 {
204 ID: providerServer.URL,
205 Path: "/",
206 URI: providerServer.URL,
207 },
208 },
209 }
211 opts.Cookie.Secure = false
212 opts.InjectRequestHeaders = []options.Header{
213 {
214 Name: "Authorization",
215 Values: []options.HeaderValue{
216 {
217 ClaimSource: &options.ClaimSource{
218 Claim: "email",
219 BasicAuthPassword: &options.SecretSource{
220 Value: []byte(basicAuthPassword),
221 },
222 },
223 },
224 },
225 },
226 }
228 err := validation.Validate(opts)
229 assert.NoError(t, err)
231 providerURL, _ := url.Parse(providerServer.URL)
232 const emailAddress = "john.doe@example.com"
234 proxy, err := NewOAuthProxy(opts, func(email string) bool {
235 return email == emailAddress
236 })
237 if err != nil {
238 t.Fatal(err)
239 }
240 proxy.provider = NewTestProvider(providerURL, emailAddress)
242 // Save the required session
243 rw := httptest.NewRecorder()
244 req, _ := http.NewRequest(http.MethodGet, "/", nil)
245 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
246 Email: emailAddress,
247 })
248 assert.NoError(t, err)
250 // Extract the cookie value to inject into the test request
251 cookie := rw.Header().Values("Set-Cookie")[0]
253 req, _ = http.NewRequest(http.MethodGet, "/", nil)
254 req.Header.Set("Cookie", cookie)
255 rw = httptest.NewRecorder()
256 proxy.ServeHTTP(rw, req)
258 // The username in the basic auth credentials is expected to be equal to the email address from the
259 // auth response, so we use the same variable here.
260 expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(emailAddress+":"+basicAuthPassword))
261 assert.Equal(t, expectedHeader, rw.Body.String())
262 providerServer.Close()
265func TestPassGroupsHeadersWithGroups(t *testing.T) {
266 opts := baseTestOptions()
267 opts.InjectRequestHeaders = []options.Header{
268 {
269 Name: "X-Forwarded-Groups",
270 Values: []options.HeaderValue{
271 {
272 ClaimSource: &options.ClaimSource{
273 Claim: "groups",
274 },
275 },
276 },
277 },
278 }
280 err := validation.Validate(opts)
281 assert.NoError(t, err)
283 const emailAddress = "john.doe@example.com"
284 const userName = "9fcab5c9b889a557"
286 groups := []string{"a", "b"}
287 created := time.Now()
288 session := &sessions.SessionState{
289 User: userName,
290 Groups: groups,
291 Email: emailAddress,
292 AccessToken: "oauth_token",
293 CreatedAt: &created,
294 }
296 proxy, err := NewOAuthProxy(opts, func(email string) bool {
297 return email == emailAddress
298 })
299 assert.NoError(t, err)
301 // Save the required session
302 rw := httptest.NewRecorder()
303 req, _ := http.NewRequest(http.MethodGet, "/", nil)
304 err = proxy.sessionStore.Save(rw, req, session)
305 assert.NoError(t, err)
307 // Extract the cookie value to inject into the test request
308 cookie := rw.Header().Values("Set-Cookie")[0]
310 req, _ = http.NewRequest(http.MethodGet, "/", nil)
311 req.Header.Set("Cookie", cookie)
312 rw = httptest.NewRecorder()
313 proxy.ServeHTTP(rw, req)
315 assert.Equal(t, []string{"a,b"}, req.Header["X-Forwarded-Groups"])
318type PassAccessTokenTest struct {
319 providerServer *httptest.Server
320 proxy *OAuthProxy
321 opts *options.Options
324type PassAccessTokenTestOptions struct {
325 PassAccessToken bool
326 ValidToken bool
327 ProxyUpstream options.Upstream
330func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) (*PassAccessTokenTest, error) {
331 patt := &PassAccessTokenTest{}
333 patt.providerServer = httptest.NewServer(
334 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
335 var payload string
336 switch r.URL.Path {
337 case "/oauth/token":
338 payload = `{"access_token": "my_auth_token"}`
339 default:
340 payload = r.Header.Get("X-Forwarded-Access-Token")
341 if payload == "" {
342 payload = "No access token found."
343 }
344 }
345 w.WriteHeader(200)
346 _, err := w.Write([]byte(payload))
347 if err != nil {
348 panic(err)
349 }
350 }))
352 patt.opts = baseTestOptions()
353 patt.opts.UpstreamServers = options.UpstreamConfig{
354 Upstreams: []options.Upstream{
355 {
356 ID: patt.providerServer.URL,
357 Path: "/",
358 URI: patt.providerServer.URL,
359 },
360 },
361 }
362 if opts.ProxyUpstream.ID != "" {
363 patt.opts.UpstreamServers.Upstreams = append(patt.opts.UpstreamServers.Upstreams, opts.ProxyUpstream)
364 }
366 patt.opts.Cookie.Secure = false
367 if opts.PassAccessToken {
368 patt.opts.InjectRequestHeaders = []options.Header{
369 {
370 Name: "X-Forwarded-Access-Token",
371 Values: []options.HeaderValue{
372 {
373 ClaimSource: &options.ClaimSource{
374 Claim: "access_token",
375 },
376 },
377 },
378 },
379 }
380 }
382 err := validation.Validate(patt.opts)
383 if err != nil {
384 return nil, err
385 }
387 providerURL, _ := url.Parse(patt.providerServer.URL)
388 const emailAddress = "michael.bland@gsa.gov"
390 testProvider := NewTestProvider(providerURL, emailAddress)
391 testProvider.ValidToken = opts.ValidToken
392 patt.proxy, err = NewOAuthProxy(patt.opts, func(email string) bool {
393 return email == emailAddress
394 })
395 patt.proxy.provider = testProvider
396 if err != nil {
397 return nil, err
398 }
399 return patt, nil
402func (patTest *PassAccessTokenTest) Close() {
403 patTest.providerServer.Close()
406func (patTest *PassAccessTokenTest) getCallbackEndpoint() (httpCode int, cookie string) {
407 rw := httptest.NewRecorder()
409 csrf, err := cookies.NewCSRF(patTest.proxy.CookieOptions, "")
410 if err != nil {
411 panic(err)
412 }
414 req, err := http.NewRequest(
415 http.MethodGet,
416 fmt.Sprintf(
417 "/oauth2/callback?code=callback_code&state=%s",
418 encodeState(csrf.HashOAuthState(), "%2F", false),
419 ),
420 strings.NewReader(""),
421 )
422 if err != nil {
423 return 0, ""
424 }
426 // rw is a dummy here, we just want the csrfCookie to add to our req
427 csrfCookie, err := csrf.SetCookie(httptest.NewRecorder(), req)
428 if err != nil {
429 panic(err)
430 }
431 req.AddCookie(csrfCookie)
433 patTest.proxy.ServeHTTP(rw, req)
435 if len(rw.Header().Values("Set-Cookie")) >= 2 {
436 cookie = rw.Header().Values("Set-Cookie")[1]
437 }
439 return rw.Code, cookie
442// getEndpointWithCookie makes a requests againt the oauthproxy with passed requestPath
443// and cookie and returns body and status code.
444func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoint string) (httpCode int, accessToken string) {
445 cookieName := patTest.proxy.CookieOptions.Name
446 var value string
447 keyPrefix := cookieName + "="
449 for _, field := range strings.Split(cookie, "; ") {
450 value = strings.TrimPrefix(field, keyPrefix)
451 if value != field {
452 break
453 }
454 value = ""
455 }
456 if value == "" {
457 return 0, ""
458 }
460 req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
461 if err != nil {
462 return 0, ""
463 }
464 req.AddCookie(&http.Cookie{
465 Name: cookieName,
466 Value: value,
467 Path: "/",
468 Expires: time.Now().Add(time.Duration(24)),
469 HttpOnly: true,
470 })
472 rw := httptest.NewRecorder()
473 patTest.proxy.ServeHTTP(rw, req)
474 return rw.Code, rw.Body.String()
477func TestForwardAccessTokenUpstream(t *testing.T) {
478 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
479 PassAccessToken: true,
480 ValidToken: true,
481 })
482 if err != nil {
483 t.Fatal(err)
484 }
485 t.Cleanup(patTest.Close)
487 // A successful validation will redirect and set the auth cookie.
488 code, cookie := patTest.getCallbackEndpoint()
489 if code != 302 {
490 t.Fatalf("expected 302; got %d", code)
491 }
492 assert.NotNil(t, cookie)
494 // Now we make a regular request; the access_token from the cookie is
495 // forwarded as the "X-Forwarded-Access-Token" header. The token is
496 // read by the test provider server and written in the response body.
497 code, payload := patTest.getEndpointWithCookie(cookie, "/")
498 if code != 200 {
499 t.Fatalf("expected 200; got %d", code)
500 }
501 assert.Equal(t, "my_auth_token", payload)
504func TestStaticProxyUpstream(t *testing.T) {
505 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
506 PassAccessToken: true,
507 ValidToken: true,
508 ProxyUpstream: options.Upstream{
509 ID: "static-proxy",
510 Path: "/static-proxy",
511 Static: ptr.To(true),
512 },
513 })
514 if err != nil {
515 t.Fatal(err)
516 }
517 t.Cleanup(patTest.Close)
519 // A successful validation will redirect and set the auth cookie.
520 code, cookie := patTest.getCallbackEndpoint()
521 if code != 302 {
522 t.Fatalf("expected 302; got %d", code)
523 }
524 assert.NotEqual(t, nil, cookie)
526 // Now we make a regular request against the upstream proxy; And validate
527 // the returned status code through the static proxy.
528 code, payload := patTest.getEndpointWithCookie(cookie, "/static-proxy")
529 if code != 200 {
530 t.Fatalf("expected 200; got %d", code)
531 }
532 assert.Equal(t, "Authenticated", payload)
535func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
536 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
537 PassAccessToken: false,
538 ValidToken: true,
539 })
540 if err != nil {
541 t.Fatal(err)
542 }
543 t.Cleanup(patTest.Close)
545 // A successful validation will redirect and set the auth cookie.
546 code, cookie := patTest.getCallbackEndpoint()
547 if code != 302 {
548 t.Fatalf("expected 302; got %d", code)
549 }
550 assert.NotEqual(t, nil, cookie)
552 // Now we make a regular request, but the access token header should
553 // not be present.
554 code, payload := patTest.getEndpointWithCookie(cookie, "/")
555 if code != 200 {
556 t.Fatalf("expected 200; got %d", code)
557 }
558 assert.Equal(t, "No access token found.", payload)
561func TestSessionValidationFailure(t *testing.T) {
562 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
563 ValidToken: false,
564 })
565 require.NoError(t, err)
566 t.Cleanup(patTest.Close)
568 // An unsuccessful validation will return 403 and not set the auth cookie.
569 code, cookie := patTest.getCallbackEndpoint()
570 assert.Equal(t, http.StatusForbidden, code)
571 assert.Equal(t, "", cookie)
574type SignInPageTest struct {
575 opts *options.Options
576 proxy *OAuthProxy
577 signInRegexp *regexp.Regexp
578 signInProviderRegexp *regexp.Regexp
581const (
582 signInRedirectPattern = `<input type="hidden" name="rd" value="(.*)">`
583 signInSkipProvider = `>Found<`
584 patternNotFound = "Did not find pattern in body: "
587func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
588 var sipTest SignInPageTest
590 sipTest.opts = baseTestOptions()
591 sipTest.opts.SkipProviderButton = skipProvider
592 err := validation.Validate(sipTest.opts)
593 if err != nil {
594 return nil, err
595 }
597 sipTest.proxy, err = NewOAuthProxy(sipTest.opts, func(email string) bool {
598 return true
599 })
600 if err != nil {
601 return nil, err
602 }
603 sipTest.signInRegexp = regexp.MustCompile(signInRedirectPattern)
604 sipTest.signInProviderRegexp = regexp.MustCompile(signInSkipProvider)
606 return &sipTest, nil
609func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
610 rw := httptest.NewRecorder()
611 req, _ := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
612 sipTest.proxy.ServeHTTP(rw, req)
613 return rw.Code, rw.Body.String()
616type AlwaysSuccessfulValidator struct {
619func (AlwaysSuccessfulValidator) Validate(_, _ string) bool {
620 return true
623func TestManualSignInStoresUserGroupsInTheSession(t *testing.T) {
624 userGroups := []string{"somegroup", "someothergroup"}
626 opts := baseTestOptions()
627 opts.HtpasswdUserGroups = userGroups
628 err := validation.Validate(opts)
629 if err != nil {
630 t.Fatal(err)
631 }
633 proxy, err := NewOAuthProxy(opts, func(email string) bool {
634 return true
635 })
636 if err != nil {
637 t.Fatal(err)
638 }
639 proxy.basicAuthValidator = AlwaysSuccessfulValidator{}
641 rw := httptest.NewRecorder()
642 formData := url.Values{}
643 formData.Set("username", "someuser")
644 formData.Set("password", "somepass")
645 signInReq, _ := http.NewRequest(http.MethodPost, "/oauth2/sign_in", strings.NewReader(formData.Encode()))
646 signInReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
647 proxy.ServeHTTP(rw, signInReq)
649 assert.Equal(t, http.StatusFound, rw.Code)
651 req, _ := http.NewRequest(http.MethodGet, "/something", strings.NewReader(formData.Encode()))
652 for _, c := range rw.Result().Cookies() {
653 req.AddCookie(c)
654 }
656 s, err := proxy.sessionStore.Load(req)
657 if err != nil {
658 t.Fatal(err)
659 }
660 assert.Equal(t, userGroups, s.Groups)
663type ManualSignInValidator struct{}
665func (ManualSignInValidator) Validate(user, password string) bool {
666 switch {
667 case user == "admin" && password == "adminPass":
668 return true
669 default:
670 return false
671 }
674func ManualSignInWithCredentials(t *testing.T, user, pass string) int {
675 opts := baseTestOptions()
676 err := validation.Validate(opts)
677 if err != nil {
678 t.Fatal(err)
679 }
681 proxy, err := NewOAuthProxy(opts, func(email string) bool {
682 return true
683 })
684 if err != nil {
685 t.Fatal(err)
686 }
688 proxy.basicAuthValidator = ManualSignInValidator{}
690 rw := httptest.NewRecorder()
691 formData := url.Values{}
692 formData.Set("username", user)
693 formData.Set("password", pass)
694 signInReq, _ := http.NewRequest(http.MethodPost, "/oauth2/sign_in", strings.NewReader(formData.Encode()))
695 signInReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
696 proxy.ServeHTTP(rw, signInReq)
698 return rw.Code
701func TestManualSignInEmptyUsernameAlert(t *testing.T) {
702 statusCode := ManualSignInWithCredentials(t, "", "")
703 assert.Equal(t, http.StatusBadRequest, statusCode)
706func TestManualSignInInvalidCredentialsAlert(t *testing.T) {
707 statusCode := ManualSignInWithCredentials(t, "admin", "")
708 assert.Equal(t, http.StatusUnauthorized, statusCode)
711func TestManualSignInCorrectCredentials(t *testing.T) {
712 statusCode := ManualSignInWithCredentials(t, "admin", "adminPass")
713 assert.Equal(t, http.StatusFound, statusCode)
716func TestSignInPageClearsExistingSessionCookie(t *testing.T) {
717 opts := baseTestOptions()
718 err := validation.Validate(opts)
719 require.NoError(t, err)
721 proxy, err := NewOAuthProxy(opts, func(string) bool {
722 return true
723 })
724 require.NoError(t, err)
726 // Create a real session cookie using the actual session store.
727 saveRW := httptest.NewRecorder()
728 saveReq := httptest.NewRequest(http.MethodGet, "/", nil)
729 err = proxy.sessionStore.Save(saveRW, saveReq, &sessions.SessionState{
730 Email: "john.doe@example.com",
731 })
732 require.NoError(t, err)
734 cookies := saveRW.Result().Cookies()
735 require.NotEmpty(t, cookies)
737 // Send that cookie to the sign-in page.
738 req := httptest.NewRequest(http.MethodGet, "/oauth2/sign_in", nil)
739 for _, c := range cookies {
740 req.AddCookie(c)
741 }
743 rw := httptest.NewRecorder()
744 proxy.ServeHTTP(rw, req)
746 assert.Equal(t, http.StatusOK, rw.Code)
748 cleared := false
749 for _, c := range rw.Result().Cookies() {
750 if c.Name == proxy.CookieOptions.Name {
751 cleared = true
752 assert.Equal(t, "", c.Value)
753 assert.Less(t, c.MaxAge, 0)
754 }
755 }
757 assert.True(t, cleared, "expected sign-in page to clear existing session cookie")
760func TestSignInPageIncludesTargetRedirect(t *testing.T) {
761 sipTest, err := NewSignInPageTest(false)
762 if err != nil {
763 t.Fatal(err)
764 }
765 const endpoint = "/some/random/endpoint"
767 code, body := sipTest.GetEndpoint(endpoint)
768 assert.Equal(t, 403, code)
770 match := sipTest.signInRegexp.FindStringSubmatch(body)
771 if match == nil {
772 t.Fatal(patternNotFound +
773 signInRedirectPattern + "\nBody:\n" + body)
774 }
775 if match[1] != endpoint {
776 t.Fatal(`expected redirect to "` + endpoint +
777 `", but was "` + match[1] + `"`)
778 }
781func TestSignInPageInvalidQueryStringReturnsBadRequest(t *testing.T) {
782 sipTest, err := NewSignInPageTest(true)
783 if err != nil {
784 t.Fatal(err)
785 }
786 const endpoint = "/?q=%va"
788 code, _ := sipTest.GetEndpoint(endpoint)
789 assert.Equal(t, 400, code)
792func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) {
793 sipTest, err := NewSignInPageTest(false)
794 if err != nil {
795 t.Fatal(err)
796 }
797 code, body := sipTest.GetEndpoint("/oauth2/sign_in")
798 assert.Equal(t, 200, code)
800 match := sipTest.signInRegexp.FindStringSubmatch(body)
801 if match == nil {
802 t.Fatal(patternNotFound +
803 signInRedirectPattern + "\nBody:\n" + body)
804 }
805 if match[1] != "/" {
806 t.Fatal(`expected redirect to "/", but was "` + match[1] + `"`)
807 }
810func TestSignInPageSkipProvider(t *testing.T) {
811 sipTest, err := NewSignInPageTest(true)
812 if err != nil {
813 t.Fatal(err)
814 }
816 endpoint := "/some/random/endpoint"
818 code, body := sipTest.GetEndpoint(endpoint)
819 assert.Equal(t, 302, code)
821 match := sipTest.signInProviderRegexp.FindStringSubmatch(body)
822 if match == nil {
823 t.Fatal(patternNotFound +
824 signInSkipProvider + "\nBody:\n" + body)
825 }
828func TestSignInPageSkipProviderDirect(t *testing.T) {
829 sipTest, err := NewSignInPageTest(true)
830 if err != nil {
831 t.Fatal(err)
832 }
834 endpoint := "/sign_in"
836 code, body := sipTest.GetEndpoint(endpoint)
837 assert.Equal(t, 302, code)
839 match := sipTest.signInProviderRegexp.FindStringSubmatch(body)
840 if match == nil {
841 t.Fatal(patternNotFound +
842 signInSkipProvider + "\nBody:\n" + body)
843 }
846type ProcessCookieTest struct {
847 opts *options.Options
848 proxy *OAuthProxy
849 rw *httptest.ResponseRecorder
850 req *http.Request
851 validateUser bool
854type ProcessCookieTestOpts struct {
855 providerValidateCookieResponse bool
858type OptionsModifier func(*options.Options)
860func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
861 var pcTest ProcessCookieTest
863 pcTest.opts = baseTestOptions()
864 for _, modifier := range modifiers {
865 modifier(pcTest.opts)
866 }
867 // First, set the CookieRefresh option so proxy.AesCipher is created,
868 // needed to encrypt the access_token.
869 pcTest.opts.Cookie.Refresh = time.Hour
870 err := validation.Validate(pcTest.opts)
871 if err != nil {
872 return nil, err
873 }
875 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
876 return pcTest.validateUser
877 })
878 if err != nil {
879 return nil, err
880 }
881 testProvider := &TestProvider{
882 ProviderData: &providers.ProviderData{},
883 ValidToken: opts.providerValidateCookieResponse,
884 }
886 groups := pcTest.opts.Providers[0].AllowedGroups
887 testProvider.ProviderData.AllowedGroups = make(map[string]struct{}, len(groups))
888 for _, group := range groups {
889 testProvider.ProviderData.AllowedGroups[group] = struct{}{}
890 }
891 pcTest.proxy.provider = testProvider
893 // Now, zero-out proxy.CookieRefresh for the cases that don't involve
894 // access_token validation.
895 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
896 pcTest.rw = httptest.NewRecorder()
897 pcTest.req, _ = http.NewRequest(http.MethodGet, "/", strings.NewReader(""))
898 pcTest.validateUser = true
899 return &pcTest, nil
902func NewProcessCookieTestWithDefaults() (*ProcessCookieTest, error) {
903 return NewProcessCookieTest(ProcessCookieTestOpts{
904 providerValidateCookieResponse: true,
905 })
908func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
909 return NewProcessCookieTest(ProcessCookieTestOpts{
910 providerValidateCookieResponse: true,
911 }, modifiers...)
914func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState) error {
915 err := p.proxy.SaveSession(p.rw, p.req, s)
916 if err != nil {
917 return err
918 }
919 for _, cookie := range p.rw.Result().Cookies() {
920 p.req.AddCookie(cookie)
921 }
922 return nil
925func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error) {
926 return p.proxy.LoadCookiedSession(p.req)
929func TestLoadCookiedSession(t *testing.T) {
930 pcTest, err := NewProcessCookieTestWithDefaults()
931 if err != nil {
932 t.Fatal(err)
933 }
935 created := time.Now()
936 startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: &created}
937 err = pcTest.SaveSession(startSession)
938 assert.NoError(t, err)
940 session, err := pcTest.LoadCookiedSession()
941 if err != nil {
942 t.Fatal(err)
943 }
944 assert.Equal(t, startSession.Email, session.Email)
945 assert.Equal(t, "", session.User)
946 assert.Equal(t, startSession.AccessToken, session.AccessToken)
949func TestProcessCookieNoCookieError(t *testing.T) {
950 pcTest, err := NewProcessCookieTestWithDefaults()
951 if err != nil {
952 t.Fatal(err)
953 }
955 session, err := pcTest.LoadCookiedSession()
956 assert.Error(t, err, "cookie \"_oauth2_proxy\" not present")
957 if session != nil {
958 t.Errorf("expected nil session. got %#v", session)
959 }
962func TestProcessCookieRefreshNotSet(t *testing.T) {
963 pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
964 opts.Cookie.Expire = time.Duration(23) * time.Hour
965 })
966 if err != nil {
967 t.Fatal(err)
968 }
970 reference := time.Now().Add(time.Duration(-2) * time.Hour)
972 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
973 err = pcTest.SaveSession(startSession)
974 assert.NoError(t, err)
976 session, err := pcTest.LoadCookiedSession()
977 assert.Equal(t, nil, err)
978 if session.Age() < time.Duration(-2)*time.Hour {
979 t.Errorf("cookie too young %v", session.Age())
980 }
981 assert.Equal(t, startSession.Email, session.Email)
984func TestProcessCookieFailIfCookieExpired(t *testing.T) {
985 pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
986 opts.Cookie.Expire = time.Duration(24) * time.Hour
987 })
988 if err != nil {
989 t.Fatal(err)
990 }
992 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
993 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
994 err = pcTest.SaveSession(startSession)
995 assert.NoError(t, err)
997 session, err := pcTest.LoadCookiedSession()
998 assert.NotEqual(t, nil, err)
999 if session != nil {
1000 t.Errorf("expected nil session %#v", session)
1004func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
1005 pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1006 opts.Cookie.Expire = time.Duration(24) * time.Hour
1007 })
1008 if err != nil {
1009 t.Fatal(err)
1012 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
1013 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1014 err = pcTest.SaveSession(startSession)
1015 assert.NoError(t, err)
1017 pcTest.proxy.CookieOptions.Refresh = time.Hour
1018 session, err := pcTest.LoadCookiedSession()
1019 assert.NotEqual(t, nil, err)
1020 if session != nil {
1021 t.Errorf("expected nil session %#v", session)
1025func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
1026 pcTest, err := NewProcessCookieTestWithDefaults()
1027 if err != nil {
1028 return nil, err
1030 pcTest.req, _ = http.NewRequest(http.MethodGet,
1031 pcTest.opts.ProxyPrefix+"/userinfo", nil)
1032 return pcTest, nil
1035func TestUserInfoEndpointAccepted(t *testing.T) {
1036 testCases := []struct {
1037 name string
1038 session *sessions.SessionState
1039 expectedResponse string
1040 }{
1042 name: "Full session",
1043 session: &sessions.SessionState{
1044 User: "john.doe",
1045 Email: "john.doe@example.com",
1046 Groups: []string{"example", "groups"},
1047 AccessToken: "my_access_token",
1048 },
1049 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"]}\n",
1050 },
1052 name: "Minimal session",
1053 session: &sessions.SessionState{
1054 User: "john.doe",
1055 Email: "john.doe@example.com",
1056 Groups: []string{"example", "groups"},
1057 },
1058 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"]}\n",
1059 },
1061 name: "No groups",
1062 session: &sessions.SessionState{
1063 User: "john.doe",
1064 Email: "john.doe@example.com",
1065 AccessToken: "my_access_token",
1066 },
1067 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\"}\n",
1068 },
1070 name: "With Preferred Username",
1071 session: &sessions.SessionState{
1072 User: "john.doe",
1073 PreferredUsername: "john",
1074 Email: "john.doe@example.com",
1075 Groups: []string{"example", "groups"},
1076 AccessToken: "my_access_token",
1077 },
1078 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\"}\n",
1079 },
1081 name: "With Additional Claim",
1082 session: &sessions.SessionState{
1083 User: "john.doe",
1084 PreferredUsername: "john",
1085 Email: "john.doe@example.com",
1086 Groups: []string{"example", "groups"},
1087 AccessToken: "my_access_token",
1088 AdditionalClaims: map[string]interface{}{
1089 "foo": "bar",
1090 },
1091 },
1092 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\",\"additionalClaims\":{\"foo\":\"bar\"}}\n",
1093 },
1096 for _, tc := range testCases {
1097 t.Run(tc.name, func(t *testing.T) {
1098 test, err := NewUserInfoEndpointTest()
1099 if err != nil {
1100 t.Fatal(err)
1102 err = test.SaveSession(tc.session)
1103 assert.NoError(t, err)
1105 test.proxy.ServeHTTP(test.rw, test.req)
1106 assert.Equal(t, http.StatusOK, test.rw.Code)
1107 bodyBytes, _ := io.ReadAll(test.rw.Body)
1108 assert.Equal(t, tc.expectedResponse, string(bodyBytes))
1109 })
1113func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1114 test, err := NewUserInfoEndpointTest()
1115 if err != nil {
1116 t.Fatal(err)
1119 test.proxy.ServeHTTP(test.rw, test.req)
1120 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1123func TestEncodedUrlsStayEncoded(t *testing.T) {
1124 encodeTest, err := NewSignInPageTest(false)
1125 if err != nil {
1126 t.Fatal(err)
1128 code, _ := encodeTest.GetEndpoint("/%2F/test1/%2F/test2")
1129 assert.Equal(t, 403, code)
1132func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1133 pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
1134 if err != nil {
1135 return nil, err
1137 pcTest.req, _ = http.NewRequest(
1138 http.MethodGet,
1139 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1140 nil)
1141 return pcTest, nil
1144func TestAuthOnlyEndpointAccepted(t *testing.T) {
1145 test, err := NewAuthOnlyEndpointTest("")
1146 if err != nil {
1147 t.Fatal(err)
1150 created := time.Now()
1151 startSession := &sessions.SessionState{
1152 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created}
1153 err = test.SaveSession(startSession)
1154 assert.NoError(t, err)
1156 test.proxy.ServeHTTP(test.rw, test.req)
1157 assert.Equal(t, http.StatusAccepted, test.rw.Code)
1158 bodyBytes, _ := io.ReadAll(test.rw.Body)
1159 assert.Equal(t, "", string(bodyBytes))
1162func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1163 test, err := NewAuthOnlyEndpointTest("")
1164 if err != nil {
1165 t.Fatal(err)
1168 test.proxy.ServeHTTP(test.rw, test.req)
1169 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1170 bodyBytes, _ := io.ReadAll(test.rw.Body)
1171 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1174func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1175 test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1176 opts.Cookie.Expire = time.Duration(24) * time.Hour
1177 })
1178 if err != nil {
1179 t.Fatal(err)
1182 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
1183 startSession := &sessions.SessionState{
1184 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1185 err = test.SaveSession(startSession)
1186 assert.NoError(t, err)
1188 test.proxy.ServeHTTP(test.rw, test.req)
1189 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1190 bodyBytes, _ := io.ReadAll(test.rw.Body)
1191 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1194func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
1195 test, err := NewAuthOnlyEndpointTest("")
1196 if err != nil {
1197 t.Fatal(err)
1200 created := time.Now()
1201 startSession := &sessions.SessionState{
1202 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created}
1203 err = test.SaveSession(startSession)
1204 assert.NoError(t, err)
1205 test.validateUser = false
1207 test.proxy.ServeHTTP(test.rw, test.req)
1208 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1209 bodyBytes, _ := io.ReadAll(test.rw.Body)
1210 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1213func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
1214 var pcTest ProcessCookieTest
1216 pcTest.opts = baseTestOptions()
1217 pcTest.opts.InjectResponseHeaders = []options.Header{
1219 Name: "X-Auth-Request-User",
1220 Values: []options.HeaderValue{
1222 ClaimSource: &options.ClaimSource{
1223 Claim: "user",
1224 },
1225 },
1226 },
1227 },
1229 Name: "X-Auth-Request-Email",
1230 Values: []options.HeaderValue{
1232 ClaimSource: &options.ClaimSource{
1233 Claim: "email",
1234 },
1235 },
1236 },
1237 },
1239 Name: "X-Auth-Request-Groups",
1240 Values: []options.HeaderValue{
1242 ClaimSource: &options.ClaimSource{
1243 Claim: "groups",
1244 },
1245 },
1246 },
1247 },
1249 Name: "X-Forwarded-Preferred-Username",
1250 Values: []options.HeaderValue{
1252 ClaimSource: &options.ClaimSource{
1253 Claim: "preferred_username",
1254 },
1255 },
1256 },
1257 },
1259 pcTest.opts.Providers[0].AllowedGroups = []string{"oauth_groups"}
1260 err := validation.Validate(pcTest.opts)
1261 assert.NoError(t, err)
1263 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
1264 return pcTest.validateUser
1265 })
1266 if err != nil {
1267 t.Fatal(err)
1269 pcTest.proxy.provider = &TestProvider{
1270 ProviderData: &providers.ProviderData{},
1271 ValidToken: true,
1274 pcTest.validateUser = true
1276 pcTest.rw = httptest.NewRecorder()
1277 pcTest.req, _ = http.NewRequest(http.MethodGet,
1278 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1280 created := time.Now()
1281 startSession := &sessions.SessionState{
1282 User: "oauth_user", Groups: []string{"oauth_groups"}, Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1283 err = pcTest.SaveSession(startSession)
1284 assert.NoError(t, err)
1286 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
1287 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
1288 assert.Equal(t, "oauth_user", pcTest.rw.Header().Get("X-Auth-Request-User"))
1289 assert.Equal(t, startSession.Groups, pcTest.rw.Header().Values("X-Auth-Request-Groups"))
1290 assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Get("X-Auth-Request-Email"))
1293func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
1294 var pcTest ProcessCookieTest
1296 pcTest.opts = baseTestOptions()
1297 pcTest.opts.InjectResponseHeaders = []options.Header{
1299 Name: "X-Auth-Request-User",
1300 Values: []options.HeaderValue{
1302 ClaimSource: &options.ClaimSource{
1303 Claim: "user",
1304 },
1305 },
1306 },
1307 },
1309 Name: "X-Auth-Request-Email",
1310 Values: []options.HeaderValue{
1312 ClaimSource: &options.ClaimSource{
1313 Claim: "email",
1314 },
1315 },
1316 },
1317 },
1319 Name: "X-Auth-Request-Groups",
1320 Values: []options.HeaderValue{
1322 ClaimSource: &options.ClaimSource{
1323 Claim: "groups",
1324 },
1325 },
1326 },
1327 },
1329 Name: "X-Forwarded-Preferred-Username",
1330 Values: []options.HeaderValue{
1332 ClaimSource: &options.ClaimSource{
1333 Claim: "preferred_username",
1334 },
1335 },
1336 },
1337 },
1339 Name: "Authorization",
1340 Values: []options.HeaderValue{
1342 ClaimSource: &options.ClaimSource{
1343 Claim: "user",
1344 BasicAuthPassword: &options.SecretSource{
1345 Value: []byte("This is a secure password"),
1346 },
1347 },
1348 },
1349 },
1350 },
1353 err := validation.Validate(pcTest.opts)
1354 assert.NoError(t, err)
1356 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
1357 return pcTest.validateUser
1358 })
1359 if err != nil {
1360 t.Fatal(err)
1362 pcTest.proxy.provider = &TestProvider{
1363 ProviderData: &providers.ProviderData{},
1364 ValidToken: true,
1367 pcTest.validateUser = true
1369 pcTest.rw = httptest.NewRecorder()
1370 pcTest.req, _ = http.NewRequest(http.MethodGet,
1371 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1373 created := time.Now()
1374 startSession := &sessions.SessionState{
1375 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1376 err = pcTest.SaveSession(startSession)
1377 assert.NoError(t, err)
1379 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
1380 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
1381 assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0])
1382 assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0])
1383 expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("oauth_user:This is a secure password"))
1384 assert.Equal(t, expectedHeader, pcTest.rw.Header().Values("Authorization")[0])
1387func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
1388 var pcTest ProcessCookieTest
1390 pcTest.opts = baseTestOptions()
1391 pcTest.opts.InjectResponseHeaders = []options.Header{
1393 Name: "X-Auth-Request-User",
1394 Values: []options.HeaderValue{
1396 ClaimSource: &options.ClaimSource{
1397 Claim: "user",
1398 },
1399 },
1400 },
1401 },
1403 Name: "X-Auth-Request-Email",
1404 Values: []options.HeaderValue{
1406 ClaimSource: &options.ClaimSource{
1407 Claim: "email",
1408 },
1409 },
1410 },
1411 },
1413 Name: "X-Auth-Request-Groups",
1414 Values: []options.HeaderValue{
1416 ClaimSource: &options.ClaimSource{
1417 Claim: "groups",
1418 },
1419 },
1420 },
1421 },
1423 Name: "X-Forwarded-Preferred-Username",
1424 Values: []options.HeaderValue{
1426 ClaimSource: &options.ClaimSource{
1427 Claim: "preferred_username",
1428 },
1429 },
1430 },
1431 },
1433 err := validation.Validate(pcTest.opts)
1434 assert.NoError(t, err)
1436 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
1437 return pcTest.validateUser
1438 })
1439 if err != nil {
1440 t.Fatal(err)
1442 pcTest.proxy.provider = &TestProvider{
1443 ProviderData: &providers.ProviderData{},
1444 ValidToken: true,
1447 pcTest.validateUser = true
1449 pcTest.rw = httptest.NewRecorder()
1450 pcTest.req, _ = http.NewRequest(http.MethodGet,
1451 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1453 created := time.Now()
1454 startSession := &sessions.SessionState{
1455 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1456 err = pcTest.SaveSession(startSession)
1457 assert.NoError(t, err)
1459 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
1460 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
1461 assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0])
1462 assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0])
1463 assert.Equal(t, 0, len(pcTest.rw.Header().Values("Authorization")), "should not have Authorization header entries")
1466func TestAuthSkippedForPreflightRequests(t *testing.T) {
1467 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1468 w.WriteHeader(200)
1469 _, err := w.Write([]byte("response"))
1470 if err != nil {
1471 t.Fatal(err)
1473 }))
1474 t.Cleanup(upstreamServer.Close)
1476 opts := baseTestOptions()
1477 opts.UpstreamServers = options.UpstreamConfig{
1478 Upstreams: []options.Upstream{
1480 ID: upstreamServer.URL,
1481 Path: "/",
1482 URI: upstreamServer.URL,
1483 },
1484 },
1486 opts.SkipAuthPreflight = true
1487 err := validation.Validate(opts)
1488 assert.NoError(t, err)
1490 upstreamURL, _ := url.Parse(upstreamServer.URL)
1492 proxy, err := NewOAuthProxy(opts, func(string) bool { return false })
1493 if err != nil {
1494 t.Fatal(err)
1496 proxy.provider = NewTestProvider(upstreamURL, "")
1497 rw := httptest.NewRecorder()
1498 req, _ := http.NewRequest(http.MethodOptions, "/preflight-request", nil)
1499 proxy.ServeHTTP(rw, req)
1501 assert.Equal(t, 200, rw.Code)
1502 assert.Equal(t, "response", rw.Body.String())
1505type SignatureAuthenticator struct {
1506 auth hmacauth.HmacAuth
1509func (v *SignatureAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) {
1510 result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
1512 var msg string
1513 switch result {
1514 case hmacauth.ResultNoSignature:
1515 msg = "no signature received"
1516 case hmacauth.ResultMatch:
1517 msg = "signatures match"
1518 case hmacauth.ResultMismatch:
1519 msg = fmt.Sprintf(
1520 "signatures do not match:\n received: %s\n computed: %s",
1521 headerSig,
1522 computedSig)
1523 default:
1524 panic("unknown result value: " + result.String())
1527 _, err := w.Write([]byte(msg))
1528 if err != nil {
1529 panic(err)
1533type SignatureTest struct {
1534 opts *options.Options
1535 upstream *httptest.Server
1536 upstreamHost string
1537 provider *httptest.Server
1538 header http.Header
1539 rw *httptest.ResponseRecorder
1540 authenticator *SignatureAuthenticator
1541 authProvider providers.Provider
1544func NewSignatureTest() (*SignatureTest, error) {
1545 opts := baseTestOptions()
1546 opts.EmailDomains = []string{"acm.org"}
1548 authenticator := &SignatureAuthenticator{}
1549 upstreamServer := httptest.NewServer(
1550 http.HandlerFunc(authenticator.Authenticate))
1551 upstreamURL, err := url.Parse(upstreamServer.URL)
1552 if err != nil {
1553 return nil, err
1555 opts.UpstreamServers = options.UpstreamConfig{
1556 Upstreams: []options.Upstream{
1558 ID: upstreamServer.URL,
1559 Path: "/",
1560 URI: upstreamServer.URL,
1561 },
1562 },
1565 providerHandler := func(w http.ResponseWriter, r *http.Request) {
1566 _, err := w.Write([]byte(`{"access_token": "my_auth_token"}`))
1567 if err != nil {
1568 panic(err)
1571 provider := httptest.NewServer(http.HandlerFunc(providerHandler))
1572 providerURL, err := url.Parse(provider.URL)
1573 if err != nil {
1574 return nil, err
1576 testProvider := NewTestProvider(providerURL, "mbland@acm.org")
1578 return &SignatureTest{
1579 opts,
1580 upstreamServer,
1581 upstreamURL.Host,
1582 provider,
1583 make(http.Header),
1584 httptest.NewRecorder(),
1585 authenticator,
1586 testProvider,
1587 }, nil
1590func (st *SignatureTest) Close() {
1591 st.provider.Close()
1592 st.upstream.Close()
1595// fakeNetConn simulates an http.Request.Body buffer that will be consumed
1596// when it is read by the hmacauth.HmacAuth if not handled properly. See:
1598// https://github.com/18F/hmacauth/pull/4
1599type fakeNetConn struct {
1600 reqBody string
1603func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
1604 if bodyLen := len(fnc.reqBody); bodyLen != 0 {
1605 copy(p, fnc.reqBody)
1606 fnc.reqBody = ""
1607 return bodyLen, io.EOF
1609 return 0, io.EOF
1612func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) error {
1613 err := validation.Validate(st.opts)
1614 if err != nil {
1615 return err
1617 proxy, err := NewOAuthProxy(st.opts, func(email string) bool { return true })
1618 if err != nil {
1619 return err
1621 proxy.provider = st.authProvider
1623 var bodyBuf io.ReadCloser
1624 if body != "" {
1625 bodyBuf = io.NopCloser(&fakeNetConn{reqBody: body})
1627 req := httptest.NewRequest(method, "/foo/bar", bodyBuf)
1628 req.Header = st.header
1630 state := &sessions.SessionState{
1631 Email: "mbland@acm.org", AccessToken: "my_access_token"}
1632 err = proxy.SaveSession(st.rw, req, state)
1633 if err != nil {
1634 return err
1636 for _, c := range st.rw.Result().Cookies() {
1637 req.AddCookie(c)
1639 // This is used by the upstream to validate the signature.
1640 st.authenticator.auth = hmacauth.NewHmacAuth(
1641 crypto.SHA1, []byte(key), upstream.SignatureHeader, upstream.SignatureHeaders)
1642 proxy.ServeHTTP(st.rw, req)
1644 return nil
1647func TestRequestSignature(t *testing.T) {
1648 testCases := map[string]struct {
1649 method string
1650 body string
1651 key string
1652 resp string
1653 }{
1654 "No request signature": {
1655 method: http.MethodGet,
1656 body: "",
1657 key: "",
1658 resp: "no signature received",
1659 },
1660 "Get request": {
1661 method: http.MethodGet,
1662 body: "",
1663 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1664 resp: "signatures match",
1665 },
1666 "Post request": {
1667 method: http.MethodPost,
1668 body: `{ "hello": "world!" }`,
1669 key: "d90df39e2d19282840252612dd7c81421a372f61",
1670 resp: "signatures match",
1671 },
1673 for name, tc := range testCases {
1674 t.Run(name, func(t *testing.T) {
1675 st, err := NewSignatureTest()
1676 if err != nil {
1677 t.Fatal(err)
1679 t.Cleanup(st.Close)
1680 if tc.key != "" {
1681 st.opts.SignatureKey = fmt.Sprintf("sha1:%s", tc.key)
1683 err = st.MakeRequestWithExpectedKey(tc.method, tc.body, tc.key)
1684 assert.NoError(t, err)
1685 assert.Equal(t, 200, st.rw.Code)
1686 assert.Equal(t, tc.resp, st.rw.Body.String())
1687 })
1691type ajaxRequestTest struct {
1692 opts *options.Options
1693 proxy *OAuthProxy
1696func newAjaxRequestTest(forceJSONErrors bool) (*ajaxRequestTest, error) {
1697 test := &ajaxRequestTest{}
1698 test.opts = baseTestOptions()
1699 test.opts.ForceJSONErrors = forceJSONErrors
1700 err := validation.Validate(test.opts)
1701 if err != nil {
1702 return nil, err
1705 test.proxy, err = NewOAuthProxy(test.opts, func(email string) bool {
1706 return true
1707 })
1708 if err != nil {
1709 return nil, err
1711 return test, nil
1714func (test *ajaxRequestTest) getEndpoint(endpoint string, header http.Header) (int, http.Header, []byte, error) {
1715 rw := httptest.NewRecorder()
1716 req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
1717 if err != nil {
1718 return 0, nil, nil, err
1720 req.Header = header
1721 test.proxy.ServeHTTP(rw, req)
1722 return rw.Code, rw.Header(), rw.Body.Bytes(), nil
1725func testAjaxUnauthorizedRequest(t *testing.T, header http.Header, forceJSONErrors bool) {
1726 test, err := newAjaxRequestTest(forceJSONErrors)
1727 if err != nil {
1728 t.Fatal(err)
1730 endpoint := "/test"
1732 code, rh, body, err := test.getEndpoint(endpoint, header)
1733 assert.NoError(t, err)
1734 assert.Equal(t, http.StatusUnauthorized, code)
1735 mime := rh.Get("Content-Type")
1736 assert.Equal(t, applicationJSON, mime)
1737 assert.Equal(t, []byte("{}"), body)
1739func TestAjaxUnauthorizedRequest1(t *testing.T) {
1740 header := make(http.Header)
1741 header.Add("accept", applicationJSON)
1743 testAjaxUnauthorizedRequest(t, header, false)
1746func TestAjaxUnauthorizedRequest2(t *testing.T) {
1747 header := make(http.Header)
1748 header.Add("Accept", applicationJSON)
1750 testAjaxUnauthorizedRequest(t, header, false)
1753func TestAjaxUnauthorizedRequestAccept1(t *testing.T) {
1754 header := make(http.Header)
1755 header.Add("Accept", "application/json, text/plain, */*")
1757 testAjaxUnauthorizedRequest(t, header, false)
1760func TestForceJSONErrorsUnauthorizedRequest(t *testing.T) {
1761 testAjaxUnauthorizedRequest(t, nil, true)
1764func TestAjaxForbiddendRequest(t *testing.T) {
1765 test, err := newAjaxRequestTest(false)
1766 if err != nil {
1767 t.Fatal(err)
1769 endpoint := "/test"
1770 header := make(http.Header)
1771 code, rh, _, err := test.getEndpoint(endpoint, header)
1772 assert.NoError(t, err)
1773 assert.Equal(t, http.StatusForbidden, code)
1774 mime := rh.Get("Content-Type")
1775 assert.NotEqual(t, applicationJSON, mime)
1778func TestClearSplitCookie(t *testing.T) {
1779 opts := baseTestOptions()
1780 opts.Cookie.Secret = base64CookieSecret
1781 opts.Cookie.Name = "oauth2"
1782 opts.Cookie.Domains = []string{"abc"}
1783 err := validation.Validate(opts)
1784 assert.NoError(t, err)
1786 store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
1787 if err != nil {
1788 t.Fatal(err)
1791 p := OAuthProxy{CookieOptions: &opts.Cookie, sessionStore: store}
1792 var rw = httptest.NewRecorder()
1793 req := httptest.NewRequest("get", "/", nil)
1795 req.AddCookie(&http.Cookie{
1796 Name: "test1",
1797 Value: "test1",
1798 })
1799 req.AddCookie(&http.Cookie{
1800 Name: "oauth2_0",
1801 Value: "oauth2_0",
1802 })
1803 req.AddCookie(&http.Cookie{
1804 Name: "oauth2_1",
1805 Value: "oauth2_1",
1806 })
1808 err = p.ClearSessionCookie(rw, req)
1809 assert.NoError(t, err)
1810 header := rw.Header()
1812 assert.Equal(t, 2, len(header["Set-Cookie"]), "should have 3 set-cookie header entries")
1815func TestClearSingleCookie(t *testing.T) {
1816 opts := baseTestOptions()
1817 opts.Cookie.Name = "oauth2"
1818 opts.Cookie.Domains = []string{"abc"}
1819 store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
1820 if err != nil {
1821 t.Fatal(err)
1824 p := OAuthProxy{CookieOptions: &opts.Cookie, sessionStore: store}
1825 var rw = httptest.NewRecorder()
1826 req := httptest.NewRequest("get", "/", nil)
1828 req.AddCookie(&http.Cookie{
1829 Name: "test1",
1830 Value: "test1",
1831 })
1832 req.AddCookie(&http.Cookie{
1833 Name: "oauth2",
1834 Value: "oauth2",
1835 })
1837 err = p.ClearSessionCookie(rw, req)
1838 assert.NoError(t, err)
1839 header := rw.Header()
1841 assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
1844type NoOpKeySet struct {
1847func (NoOpKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
1848 splitStrings := strings.Split(jwt, ".")
1849 payloadString := splitStrings[1]
1850 return base64.RawURLEncoding.DecodeString(payloadString)
1853func TestGetJwtSession(t *testing.T) {
1854 /* token payload:
1856 "sub": "1234567890",
1857 "aud": "https://test.myapp.com",
1858 "name": "John Doe",
1859 "email": "john@example.com",
1860 "iss": "https://issuer.example.com",
1861 "iat": 1553691215,
1862 "exp": 1912151821
1864 */
1865 goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
1866 "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
1867 "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
1868 "E1LCJleHAiOjE5MTIxNTE4MjF9." +
1869 "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
1870 "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
1872 keyset := NoOpKeySet{}
1873 verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1874 &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true,
1875 SkipClientIDCheck: true})
1876 verificationOptions := internaloidc.IDTokenVerificationOptions{
1877 AudienceClaims: []string{"aud"},
1878 ClientID: "https://test.myapp.com",
1879 ExtraAudiences: []string{},
1881 internalVerifier := internaloidc.NewVerifier(verifier, verificationOptions)
1883 test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1884 opts.InjectRequestHeaders = []options.Header{
1886 Name: "Authorization",
1887 Values: []options.HeaderValue{
1889 ClaimSource: &options.ClaimSource{
1890 Claim: "id_token",
1891 Prefix: "Bearer ",
1892 },
1893 },
1894 },
1895 },
1897 Name: "X-Forwarded-User",
1898 Values: []options.HeaderValue{
1900 ClaimSource: &options.ClaimSource{
1901 Claim: "user",
1902 },
1903 },
1904 },
1905 },
1907 Name: "X-Forwarded-Email",
1908 Values: []options.HeaderValue{
1910 ClaimSource: &options.ClaimSource{
1911 Claim: "email",
1912 },
1913 },
1914 },
1915 },
1918 opts.InjectResponseHeaders = []options.Header{
1920 Name: "Authorization",
1921 Values: []options.HeaderValue{
1923 ClaimSource: &options.ClaimSource{
1924 Claim: "id_token",
1925 Prefix: "Bearer ",
1926 },
1927 },
1928 },
1929 },
1931 Name: "X-Auth-Request-User",
1932 Values: []options.HeaderValue{
1934 ClaimSource: &options.ClaimSource{
1935 Claim: "user",
1936 },
1937 },
1938 },
1939 },
1941 Name: "X-Auth-Request-Email",
1942 Values: []options.HeaderValue{
1944 ClaimSource: &options.ClaimSource{
1945 Claim: "email",
1946 },
1947 },
1948 },
1949 },
1951 opts.SkipJwtBearerTokens = true
1952 opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), internalVerifier))
1953 })
1954 if err != nil {
1955 t.Fatal(err)
1957 tp, _ := test.proxy.provider.(*TestProvider)
1958 tp.GroupValidator = func(s string) bool {
1959 return true
1962 authHeader := fmt.Sprintf("Bearer %s", goodJwt)
1963 test.req.Header = map[string][]string{
1964 "Authorization": {authHeader},
1967 test.proxy.ServeHTTP(test.rw, test.req)
1968 if test.rw.Code >= 400 {
1969 t.Fatalf("expected 3xx got %d", test.rw.Code)
1972 // Check PassAuthorization, should overwrite Basic header
1973 assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
1974 assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "1234567890")
1975 assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
1977 // SetAuthorization and SetXAuthRequest
1978 assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
1979 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "1234567890")
1980 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
1983func Test_prepareNoCache(t *testing.T) {
1984 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1985 prepareNoCache(w)
1986 })
1987 mux := http.NewServeMux()
1988 mux.Handle("/", handler)
1990 rec := httptest.NewRecorder()
1991 req := httptest.NewRequest(http.MethodGet, "/", nil)
1992 mux.ServeHTTP(rec, req)
1994 for k, v := range noCacheHeaders {
1995 assert.Equal(t, rec.Header().Get(k), v)
1999func Test_noCacheHeaders(t *testing.T) {
2000 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2001 _, err := w.Write([]byte("upstream"))
2002 if err != nil {
2003 t.Error(err)
2005 }))
2006 t.Cleanup(upstreamServer.Close)
2008 opts := baseTestOptions()
2009 opts.UpstreamServers = options.UpstreamConfig{
2010 Upstreams: []options.Upstream{
2012 ID: upstreamServer.URL,
2013 Path: "/",
2014 URI: upstreamServer.URL,
2015 },
2016 },
2018 opts.SkipAuthRegex = []string{".*"}
2019 err := validation.Validate(opts)
2020 assert.NoError(t, err)
2021 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2022 if err != nil {
2023 t.Fatal(err)
2026 t.Run("not exist in response from upstream", func(t *testing.T) {
2027 rec := httptest.NewRecorder()
2028 req := httptest.NewRequest(http.MethodGet, "/upstream", nil)
2029 proxy.ServeHTTP(rec, req)
2031 assert.Equal(t, http.StatusOK, rec.Code)
2032 assert.Equal(t, "upstream", rec.Body.String())
2034 // checking noCacheHeaders does not exists in response headers from upstream
2035 for k := range noCacheHeaders {
2036 assert.Equal(t, "", rec.Header().Get(k))
2038 })
2040 t.Run("has no-cache", func(t *testing.T) {
2041 tests := []struct {
2042 path string
2043 hasNoCache bool
2044 }{
2046 path: "/oauth2/sign_in",
2047 hasNoCache: true,
2048 },
2050 path: "/oauth2/sign_out",
2051 hasNoCache: true,
2052 },
2054 path: "/oauth2/start",
2055 hasNoCache: true,
2056 },
2058 path: "/oauth2/callback",
2059 hasNoCache: true,
2060 },
2062 path: "/oauth2/auth",
2063 hasNoCache: false,
2064 },
2066 path: "/oauth2/userinfo",
2067 hasNoCache: true,
2068 },
2070 path: "/upstream",
2071 hasNoCache: false,
2072 },
2075 for _, tt := range tests {
2076 t.Run(tt.path, func(t *testing.T) {
2077 rec := httptest.NewRecorder()
2078 req := httptest.NewRequest(http.MethodGet, tt.path, nil)
2079 proxy.ServeHTTP(rec, req)
2080 cacheControl := rec.Result().Header.Get("Cache-Control")
2081 if tt.hasNoCache != (strings.Contains(cacheControl, "no-cache")) {
2082 t.Errorf(`unexpected "Cache-Control" header: %s`, cacheControl)
2084 })
2087 })
2090func TestSignOutCallsBackendLogoutURL(t *testing.T) {
2091 const testIDToken = "test-id-token-12345"
2092 var receivedURL string
2093 backendLogoutServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
2094 receivedURL = req.URL.String()
2095 rw.WriteHeader(http.StatusOK)
2096 }))
2097 defer backendLogoutServer.Close()
2099 opts := baseTestOptions()
2100 opts.Providers[0].BackendLogoutURL = backendLogoutServer.URL + "/logout?id_token_hint={id_token}"
2101 err := validation.Validate(opts)
2102 require.NoError(t, err)
2104 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
2105 require.NoError(t, err)
2107 // Save a session with IDToken so backend logout can use it
2108 rw := httptest.NewRecorder()
2109 req := httptest.NewRequest(http.MethodGet, "/", nil)
2110 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
2111 Email: "user@example.com",
2112 IDToken: testIDToken,
2113 })
2114 require.NoError(t, err)
2115 cookie := rw.Header().Values("Set-Cookie")[0]
2117 // Hit sign_out with the session cookie; backend logout should be called before session is cleared
2118 signOutReq := httptest.NewRequest(http.MethodGet, "/oauth2/sign_out", nil)
2119 signOutReq.Header.Set("Cookie", cookie)
2120 rec := httptest.NewRecorder()
2121 proxy.ServeHTTP(rec, signOutReq)
2123 assert.Equal(t, http.StatusFound, rec.Code, "sign_out should redirect")
2124 assert.Contains(t, receivedURL, "id_token_hint="+testIDToken,
2125 "backend logout URL should have been called with id_token from session")
2128func baseTestOptions() *options.Options {
2129 opts := options.NewOptions()
2130 opts.Cookie.Secret = rawCookieSecret
2131 opts.Providers[0].ID = "providerID"
2132 opts.Providers[0].ClientID = clientID
2133 opts.Providers[0].ClientSecret = clientSecret
2134 opts.EmailDomains = []string{"*"}
2136 // Default injected headers for legacy configuration
2137 opts.InjectRequestHeaders = []options.Header{
2139 Name: "Authorization",
2140 Values: []options.HeaderValue{
2142 ClaimSource: &options.ClaimSource{
2143 Claim: "user",
2144 BasicAuthPassword: &options.SecretSource{
2145 Value: []byte(base64.StdEncoding.EncodeToString([]byte("This is a secure password"))),
2146 },
2147 },
2148 },
2149 },
2150 },
2152 Name: "X-Forwarded-User",
2153 Values: []options.HeaderValue{
2155 ClaimSource: &options.ClaimSource{
2156 Claim: "user",
2157 },
2158 },
2159 },
2160 },
2162 Name: "X-Forwarded-Email",
2163 Values: []options.HeaderValue{
2165 ClaimSource: &options.ClaimSource{
2166 Claim: "email",
2167 },
2168 },
2169 },
2170 },
2173 return opts
2176func TestTrustedIPs(t *testing.T) {
2177 tests := []struct {
2178 name string
2179 trustedIPs []string
2180 reverseProxy bool
2181 realClientIPHeader string
2182 req *http.Request
2183 expectTrusted bool
2184 }{
2185 // Check unconfigured behavior.
2187 name: "Default",
2188 trustedIPs: nil,
2189 reverseProxy: false,
2190 realClientIPHeader: "X-Real-IP", // Default value
2191 req: func() *http.Request {
2192 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2193 return req
2194 }(),
2195 expectTrusted: false,
2196 },
2197 // Check Unix socket with no trusted IPs configured does not error.
2199 name: "UnixSocketWithoutTrustedIPs",
2200 trustedIPs: nil,
2201 reverseProxy: false,
2202 realClientIPHeader: "X-Real-IP",
2203 req: func() *http.Request {
2204 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2205 req.RemoteAddr = "@"
2206 return req
2207 }(),
2208 expectTrusted: false,
2209 },
2210 // Check Unix socket with trusted IPs configured returns false (no IP to match).
2212 name: "UnixSocketWithTrustedIPs",
2213 trustedIPs: []string{"127.0.0.1"},
2214 reverseProxy: false,
2215 realClientIPHeader: "X-Real-IP",
2216 req: func() *http.Request {
2217 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2218 req.RemoteAddr = "@"
2219 return req
2220 }(),
2221 expectTrusted: false,
2222 },
2223 // Check using req.RemoteAddr (Options.ReverseProxy == false).
2225 name: "WithRemoteAddr",
2226 trustedIPs: []string{"127.0.0.1"},
2227 reverseProxy: false,
2228 realClientIPHeader: "X-Real-IP", // Default value
2229 req: func() *http.Request {
2230 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2231 req.RemoteAddr = "127.0.0.1:43670"
2232 return req
2233 }(),
2234 expectTrusted: true,
2235 },
2236 // Check ignores req.RemoteAddr match when behind a reverse proxy / missing header.
2238 name: "IgnoresRemoteAddrInReverseProxyMode",
2239 trustedIPs: []string{"127.0.0.1"},
2240 reverseProxy: true,
2241 realClientIPHeader: "X-Real-IP", // Default value
2242 req: func() *http.Request {
2243 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2244 req.RemoteAddr = "127.0.0.1:44324"
2245 return req
2246 }(),
2247 expectTrusted: false,
2248 },
2249 // Check successful trusting of localhost in IPv4.
2251 name: "TrustsLocalhostInReverseProxyMode",
2252 trustedIPs: []string{"127.0.0.0/8", "::1"},
2253 reverseProxy: true,
2254 realClientIPHeader: "X-Forwarded-For",
2255 req: func() *http.Request {
2256 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2257 req.Header.Add("X-Forwarded-For", "127.0.0.1")
2258 return req
2259 }(),
2260 expectTrusted: true,
2261 },
2262 // Check successful trusting of localhost in IPv6.
2264 name: "TrustsIP6LocalostInReverseProxyMode",
2265 trustedIPs: []string{"127.0.0.0/8", "::1"},
2266 reverseProxy: true,
2267 realClientIPHeader: "X-Forwarded-For",
2268 req: func() *http.Request {
2269 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2270 req.Header.Add("X-Forwarded-For", "::1")
2271 return req
2272 }(),
2273 expectTrusted: true,
2274 },
2275 // Check does not trust random IPv4 address.
2277 name: "DoesNotTrustRandomIP4Address",
2278 trustedIPs: []string{"127.0.0.0/8", "::1"},
2279 reverseProxy: true,
2280 realClientIPHeader: "X-Forwarded-For",
2281 req: func() *http.Request {
2282 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2283 req.Header.Add("X-Forwarded-For", "12.34.56.78")
2284 return req
2285 }(),
2286 expectTrusted: false,
2287 },
2288 // Check does not trust random IPv6 address.
2290 name: "DoesNotTrustRandomIP6Address",
2291 trustedIPs: []string{"127.0.0.0/8", "::1"},
2292 reverseProxy: true,
2293 realClientIPHeader: "X-Forwarded-For",
2294 req: func() *http.Request {
2295 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2296 req.Header.Add("X-Forwarded-For", "::2")
2297 return req
2298 }(),
2299 expectTrusted: false,
2300 },
2301 // Check respects correct header.
2303 name: "RespectsCorrectHeaderInReverseProxyMode",
2304 trustedIPs: []string{"127.0.0.0/8", "::1"},
2305 reverseProxy: true,
2306 realClientIPHeader: "X-Forwarded-For",
2307 req: func() *http.Request {
2308 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2309 req.Header.Add("X-Real-IP", "::1")
2310 return req
2311 }(),
2312 expectTrusted: false,
2313 },
2314 // Check doesn't trust if garbage is provided.
2316 name: "DoesNotTrustGarbageInReverseProxyMode",
2317 trustedIPs: []string{"127.0.0.0/8", "::1"},
2318 reverseProxy: true,
2319 realClientIPHeader: "X-Forwarded-For",
2320 req: func() *http.Request {
2321 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2322 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2323 return req
2324 }(),
2325 expectTrusted: false,
2326 },
2327 // Check doesn't trust if garbage is provided (no reverse-proxy).
2329 name: "DoesNotTrustGarbage",
2330 trustedIPs: []string{"127.0.0.0/8", "::1"},
2331 reverseProxy: false,
2332 realClientIPHeader: "X-Real-IP",
2333 req: func() *http.Request {
2334 req, _ := http.NewRequest(http.MethodGet, "/", nil)
2335 req.RemoteAddr = "adsfljk29242as!!"
2336 return req
2337 }(),
2338 expectTrusted: false,
2339 },
2342 for _, tt := range tests {
2343 t.Run(tt.name, func(t *testing.T) {
2344 opts := baseTestOptions()
2345 opts.UpstreamServers = options.UpstreamConfig{
2346 Upstreams: []options.Upstream{
2348 ID: "static",
2349 Path: "/",
2350 Static: ptr.To(true),
2351 },
2352 },
2354 opts.TrustedIPs = tt.trustedIPs
2355 opts.ReverseProxy = tt.reverseProxy
2356 opts.RealClientIPHeader = tt.realClientIPHeader
2357 err := validation.Validate(opts)
2358 assert.NoError(t, err)
2360 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
2361 assert.NoError(t, err)
2362 rw := httptest.NewRecorder()
2364 proxy.ServeHTTP(rw, tt.req)
2365 if tt.expectTrusted {
2366 assert.Equal(t, 200, rw.Code)
2367 } else {
2368 assert.Equal(t, 403, rw.Code)
2370 })
2374func Test_buildRoutesAllowlist(t *testing.T) {
2375 type expectedAllowedRoute struct {
2376 method string
2377 negate bool
2378 regexString string
2381 testCases := []struct {
2382 name string
2383 skipAuthRegex []string
2384 skipAuthRoutes []string
2385 expectedRoutes []expectedAllowedRoute
2386 shouldError bool
2387 }{
2389 name: "No skip auth configured",
2390 skipAuthRegex: []string{},
2391 skipAuthRoutes: []string{},
2392 expectedRoutes: []expectedAllowedRoute{},
2393 shouldError: false,
2394 },
2396 name: "Only skipAuthRegex configured",
2397 skipAuthRegex: []string{
2398 "^/foo/bar",
2399 "^/baz/[0-9]+/thing",
2400 },
2401 skipAuthRoutes: []string{},
2402 expectedRoutes: []expectedAllowedRoute{
2404 method: "",
2405 negate: false,
2406 regexString: "^/foo/bar",
2407 },
2409 method: "",
2410 negate: false,
2411 regexString: "^/baz/[0-9]+/thing",
2412 },
2413 },
2414 shouldError: false,
2415 },
2417 name: "Only skipAuthRoutes configured",
2418 skipAuthRegex: []string{},
2419 skipAuthRoutes: []string{
2420 "GET=^/foo/bar",
2421 "POST=^/baz/[0-9]+/thing",
2422 "^/all/methods$",
2423 "WEIRD=^/methods/are/allowed",
2424 "PATCH=/second/equals?are=handled&just=fine",
2425 "!=^/api",
2426 "METHOD!=^/api",
2427 },
2428 expectedRoutes: []expectedAllowedRoute{
2430 method: http.MethodGet,
2431 negate: false,
2432 regexString: "^/foo/bar",
2433 },
2435 method: http.MethodPost,
2436 negate: false,
2437 regexString: "^/baz/[0-9]+/thing",
2438 },
2440 method: "",
2441 negate: false,
2442 regexString: "^/all/methods$",
2443 },
2445 method: "WEIRD",
2446 negate: false,
2447 regexString: "^/methods/are/allowed",
2448 },
2450 method: "PATCH",
2451 negate: false,
2452 regexString: "/second/equals?are=handled&just=fine",
2453 },
2455 method: "",
2456 negate: true,
2457 regexString: "^/api",
2458 },
2460 method: "METHOD",
2461 negate: true,
2462 regexString: "^/api",
2463 },
2464 },
2465 shouldError: false,
2466 },
2468 name: "Both skipAuthRegexes and skipAuthRoutes configured",
2469 skipAuthRegex: []string{
2470 "^/foo/bar/regex",
2471 "^/baz/[0-9]+/thing/regex",
2472 },
2473 skipAuthRoutes: []string{
2474 "GET=^/foo/bar",
2475 "POST=^/baz/[0-9]+/thing",
2476 "^/all/methods$",
2477 },
2478 expectedRoutes: []expectedAllowedRoute{
2480 method: "",
2481 regexString: "^/foo/bar/regex",
2482 },
2484 method: "",
2485 regexString: "^/baz/[0-9]+/thing/regex",
2486 },
2488 method: http.MethodGet,
2489 regexString: "^/foo/bar",
2490 },
2492 method: http.MethodPost,
2493 regexString: "^/baz/[0-9]+/thing",
2494 },
2496 method: "",
2497 regexString: "^/all/methods$",
2498 },
2499 },
2500 shouldError: false,
2501 },
2503 name: "Invalid skipAuthRegex entry",
2504 skipAuthRegex: []string{
2505 "^/foo/bar",
2506 "^/baz/[0-9]+/thing",
2507 "(bad[regex",
2508 },
2509 skipAuthRoutes: []string{},
2510 expectedRoutes: []expectedAllowedRoute{},
2511 shouldError: true,
2512 },
2514 name: "Invalid skipAuthRoutes entry",
2515 skipAuthRegex: []string{},
2516 skipAuthRoutes: []string{
2517 "GET=^/foo/bar",
2518 "POST=^/baz/[0-9]+/thing",
2519 "^/all/methods$",
2520 "PUT=(bad[regex",
2521 },
2522 expectedRoutes: []expectedAllowedRoute{},
2523 shouldError: true,
2524 },
2527 for _, tc := range testCases {
2528 t.Run(tc.name, func(t *testing.T) {
2529 opts := &options.Options{
2530 SkipAuthRegex: tc.skipAuthRegex,
2531 SkipAuthRoutes: tc.skipAuthRoutes,
2533 routes, err := buildRoutesAllowlist(opts)
2534 if tc.shouldError {
2535 assert.Error(t, err)
2536 return
2538 assert.NoError(t, err)
2540 for i, route := range routes {
2541 assert.Greater(t, len(tc.expectedRoutes), i)
2542 assert.Equal(t, route.method, tc.expectedRoutes[i].method)
2543 assert.Equal(t, route.negate, tc.expectedRoutes[i].negate)
2544 assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString)
2546 })
2550func TestApiRoutes(t *testing.T) {
2552 ajaxAPIServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2553 w.WriteHeader(200)
2554 _, err := w.Write([]byte("AJAX API Request"))
2555 if err != nil {
2556 t.Fatal(err)
2558 }))
2559 t.Cleanup(ajaxAPIServer.Close)
2561 apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2562 w.WriteHeader(200)
2563 _, err := w.Write([]byte("API Request"))
2564 if err != nil {
2565 t.Fatal(err)
2567 }))
2568 t.Cleanup(apiServer.Close)
2570 uiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2571 w.WriteHeader(200)
2572 _, err := w.Write([]byte("API Request"))
2573 if err != nil {
2574 t.Fatal(err)
2576 }))
2577 t.Cleanup(uiServer.Close)
2579 opts := baseTestOptions()
2580 opts.UpstreamServers = options.UpstreamConfig{
2581 Upstreams: []options.Upstream{
2583 ID: apiServer.URL,
2584 Path: "/api",
2585 URI: apiServer.URL,
2586 },
2588 ID: ajaxAPIServer.URL,
2589 Path: "/ajaxapi",
2590 URI: ajaxAPIServer.URL,
2591 },
2593 ID: uiServer.URL,
2594 Path: "/ui",
2595 URI: uiServer.URL,
2596 },
2597 },
2599 opts.APIRoutes = []string{
2600 "^/api",
2602 opts.SkipProviderButton = true
2603 err := validation.Validate(opts)
2604 assert.NoError(t, err)
2605 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2606 if err != nil {
2607 t.Fatal(err)
2610 testCases := []struct {
2611 name string
2612 contentType string
2613 url string
2614 shouldRedirect bool
2615 }{
2617 name: "AJAX request matching API regex",
2618 contentType: "application/json",
2619 url: "/api/v1/UserInfo",
2620 shouldRedirect: false,
2621 },
2623 name: "AJAX request not matching API regex",
2624 contentType: "application/json",
2625 url: "/ajaxapi/v1/UserInfo",
2626 shouldRedirect: false,
2627 },
2629 name: "Other Request matching API regex",
2630 contentType: "application/grpcwebtext",
2631 url: "/api/v1/UserInfo",
2632 shouldRedirect: false,
2633 },
2635 name: "UI request",
2636 contentType: "html",
2637 url: "/ui/index.html",
2638 shouldRedirect: true,
2639 },
2642 for _, tc := range testCases {
2643 t.Run(tc.name, func(t *testing.T) {
2644 req, err := http.NewRequest(http.MethodGet, tc.url, nil)
2645 req.Header.Set("Accept", tc.contentType)
2646 assert.NoError(t, err)
2648 rw := httptest.NewRecorder()
2649 proxy.ServeHTTP(rw, req)
2651 if tc.shouldRedirect {
2652 assert.Equal(t, 302, rw.Code)
2653 } else {
2654 assert.Equal(t, 401, rw.Code)
2656 })
2660func TestAllowedRequest(t *testing.T) {
2661 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2662 w.WriteHeader(200)
2663 _, err := w.Write([]byte("Allowed Request"))
2664 if err != nil {
2665 t.Fatal(err)
2667 }))
2668 t.Cleanup(upstreamServer.Close)
2670 opts := baseTestOptions()
2671 opts.UpstreamServers = options.UpstreamConfig{
2672 Upstreams: []options.Upstream{
2674 ID: upstreamServer.URL,
2675 Path: "/",
2676 URI: upstreamServer.URL,
2677 },
2678 },
2680 opts.SkipAuthRegex = []string{
2681 "^/skip/auth/regex$",
2682 "^/public/.*/endpoint$",
2684 opts.SkipAuthRoutes = []string{
2685 "GET=^/skip/auth/routes/get",
2686 "^/foo/.*/bar$",
2688 err := validation.Validate(opts)
2689 assert.NoError(t, err)
2690 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2691 if err != nil {
2692 t.Fatal(err)
2695 testCases := []struct {
2696 name string
2697 method string
2698 url string
2699 allowed bool
2700 }{
2702 name: "Regex GET allowed",
2703 method: http.MethodGet,
2704 url: "/skip/auth/regex",
2705 allowed: true,
2706 },
2708 name: "Regex POST allowed ",
2709 method: http.MethodPost,
2710 url: "/skip/auth/regex",
2711 allowed: true,
2712 },
2714 name: "Regex denied",
2715 method: http.MethodGet,
2716 url: "/wrong/denied",
2717 allowed: false,
2718 },
2720 name: "Regex allowed with fragment-free path",
2721 method: http.MethodGet,
2722 url: "/public/legit/endpoint",
2723 allowed: true,
2724 },
2726 name: "Regex denied when path contains encoded fragment suffix",
2727 method: http.MethodGet,
2728 url: "/public/secret%23/endpoint",
2729 allowed: false,
2730 },
2732 name: "Route allowed",
2733 method: http.MethodGet,
2734 url: "/skip/auth/routes/get",
2735 allowed: true,
2736 },
2738 name: "Route denied with wrong method",
2739 method: "PATCH",
2740 url: "/skip/auth/routes/get",
2741 allowed: false,
2742 },
2744 name: "Route denied with wrong path",
2745 method: http.MethodGet,
2746 url: "/skip/auth/routes/wrong/path",
2747 allowed: false,
2748 },
2750 name: "Route denied with wrong path and method",
2751 method: http.MethodPost,
2752 url: "/skip/auth/routes/wrong/path",
2753 allowed: false,
2754 },
2756 name: "Route allowed with fragment-free path",
2757 method: http.MethodGet,
2758 url: "/foo/public/bar",
2759 allowed: true,
2760 },
2762 name: "Route denied when path contains encoded fragment suffix",
2763 method: http.MethodGet,
2764 url: "/foo/secret%23/bar",
2765 allowed: false,
2766 },
2769 for _, tc := range testCases {
2770 t.Run(tc.name, func(t *testing.T) {
2771 req, err := http.NewRequest(tc.method, tc.url, nil)
2772 assert.NoError(t, err)
2773 assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req))
2775 rw := httptest.NewRecorder()
2776 proxy.ServeHTTP(rw, req)
2778 if tc.allowed {
2779 assert.Equal(t, 200, rw.Code)
2780 assert.Equal(t, "Allowed Request", rw.Body.String())
2781 } else {
2782 assert.Equal(t, 403, rw.Code)
2784 })
2788func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2789 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2790 w.WriteHeader(200)
2791 }))
2792 t.Cleanup(upstreamServer.Close)
2794 opts := baseTestOptions()
2795 opts.ReverseProxy = true
2796 opts.UpstreamServers = options.UpstreamConfig{
2797 Upstreams: []options.Upstream{
2799 ID: upstreamServer.URL,
2800 Path: "/",
2801 URI: upstreamServer.URL,
2802 },
2803 },
2805 opts.SkipAuthRegex = []string{
2806 "^/skip/auth/regex$",
2807 "^/public/.*/endpoint$",
2809 opts.SkipAuthRoutes = []string{
2810 "GET=^/skip/auth/routes/get",
2811 "^/foo/.*/bar$",
2813 err := validation.Validate(opts)
2814 assert.NoError(t, err)
2815 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2816 if err != nil {
2817 t.Fatal(err)
2820 testCases := []struct {
2821 name string
2822 method string
2823 url string
2824 allowed bool
2825 }{
2827 name: "Regex GET allowed",
2828 method: http.MethodGet,
2829 url: "/skip/auth/regex",
2830 allowed: true,
2831 },
2833 name: "Regex POST allowed ",
2834 method: http.MethodPost,
2835 url: "/skip/auth/regex",
2836 allowed: true,
2837 },
2839 name: "Regex denied",
2840 method: http.MethodGet,
2841 url: "/wrong/denied",
2842 allowed: false,
2843 },
2845 name: "Regex allowed with fragment-free path",
2846 method: http.MethodGet,
2847 url: "/public/legit/endpoint",
2848 allowed: true,
2849 },
2851 name: "Regex denied when X-Forwarded-Uri contains an encoded fragment suffix",
2852 method: http.MethodGet,
2853 url: "/public/secret%23/endpoint",
2854 allowed: false,
2855 },
2857 name: "Route allowed",
2858 method: http.MethodGet,
2859 url: "/skip/auth/routes/get",
2860 allowed: true,
2861 },
2863 name: "Route denied with wrong method",
2864 method: "PATCH",
2865 url: "/skip/auth/routes/get",
2866 allowed: false,
2867 },
2869 name: "Route denied with wrong path",
2870 method: http.MethodGet,
2871 url: "/skip/auth/routes/wrong/path",
2872 allowed: false,
2873 },
2875 name: "Route denied with wrong path and method",
2876 method: http.MethodPost,
2877 url: "/skip/auth/routes/wrong/path",
2878 allowed: false,
2879 },
2881 name: "Route allowed with fragment-free path",
2882 method: http.MethodGet,
2883 url: "/foo/public/bar",
2884 allowed: true,
2885 },
2887 name: "Route denied when X-Forwarded-Uri contains an encoded fragment suffix",
2888 method: http.MethodGet,
2889 url: "/foo/secret%23/bar",
2890 allowed: false,
2891 },
2894 for _, tc := range testCases {
2895 t.Run(tc.name, func(t *testing.T) {
2896 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
2897 req.Header.Set("X-Forwarded-Uri", tc.url)
2898 req.RemoteAddr = "127.0.0.1:4180"
2899 assert.NoError(t, err)
2901 rw := httptest.NewRecorder()
2902 proxy.ServeHTTP(rw, req)
2904 if tc.allowed {
2905 assert.Equal(t, 202, rw.Code)
2906 } else {
2907 assert.Equal(t, 401, rw.Code)
2909 })
2913func TestAllowedRequestWithForwardedUriHeaderRequiresTrustedProxy(t *testing.T) {
2914 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2915 w.WriteHeader(200)
2916 }))
2917 t.Cleanup(upstreamServer.Close)
2919 opts := baseTestOptions()
2920 opts.ReverseProxy = true
2921 opts.TrustedProxyIPs = []string{"127.0.0.1/32"}
2922 opts.UpstreamServers = options.UpstreamConfig{
2923 Upstreams: []options.Upstream{
2925 ID: upstreamServer.URL,
2926 Path: "/",
2927 URI: upstreamServer.URL,
2928 },
2929 },
2931 opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
2933 err := validation.Validate(opts)
2934 assert.NoError(t, err)
2936 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2937 assert.NoError(t, err)
2939 req, err := http.NewRequest(http.MethodGet, opts.ProxyPrefix+authOnlyPath, nil)
2940 assert.NoError(t, err)
2941 req.RemoteAddr = "192.0.2.10:4180"
2942 req.Header.Set("X-Forwarded-Uri", "/skip/auth/regex")
2944 rw := httptest.NewRecorder()
2945 proxy.ServeHTTP(rw, req)
2947 assert.Equal(t, 401, rw.Code)
2950func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2951 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2952 w.WriteHeader(200)
2953 _, err := w.Write([]byte("Allowed Request"))
2954 if err != nil {
2955 t.Fatal(err)
2957 }))
2958 t.Cleanup(upstreamServer.Close)
2960 opts := baseTestOptions()
2961 opts.UpstreamServers = options.UpstreamConfig{
2962 Upstreams: []options.Upstream{
2964 ID: upstreamServer.URL,
2965 Path: "/",
2966 URI: upstreamServer.URL,
2967 },
2968 },
2970 opts.SkipAuthRoutes = []string{
2971 "!=^/api", // any non-api routes
2972 "POST=^/api/public-entity/?$",
2974 err := validation.Validate(opts)
2975 assert.NoError(t, err)
2976 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2977 if err != nil {
2978 t.Fatal(err)
2981 testCases := []struct {
2982 name string
2983 method string
2984 url string
2985 allowed bool
2986 }{
2988 name: "Some static file allowed",
2989 method: http.MethodGet,
2990 url: "/static/file.txt",
2991 allowed: true,
2992 },
2994 name: "POST to contact form allowed",
2995 method: http.MethodPost,
2996 url: "/contact",
2997 allowed: true,
2998 },
3000 name: "Regex POST allowed",
3001 method: http.MethodPost,
3002 url: "/api/public-entity",
3003 allowed: true,
3004 },
3006 name: "Regex POST with trailing slash allowed",
3007 method: http.MethodPost,
3008 url: "/api/public-entity/",
3009 allowed: true,
3010 },
3012 name: "Regex GET api route denied",
3013 method: http.MethodGet,
3014 url: "/api/users",
3015 allowed: false,
3016 },
3018 name: "Regex POST api route denied",
3019 method: http.MethodPost,
3020 url: "/api/users",
3021 allowed: false,
3022 },
3024 name: "Regex DELETE api route denied",
3025 method: "DELETE",
3026 url: "/api/users/1",
3027 allowed: false,
3028 },
3031 for _, tc := range testCases {
3032 t.Run(tc.name, func(t *testing.T) {
3033 req, err := http.NewRequest(tc.method, tc.url, nil)
3034 assert.NoError(t, err)
3035 assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req))
3037 rw := httptest.NewRecorder()
3038 proxy.ServeHTTP(rw, req)
3040 if tc.allowed {
3041 assert.Equal(t, 200, rw.Code)
3042 assert.Equal(t, "Allowed Request", rw.Body.String())
3043 } else {
3044 assert.Equal(t, 403, rw.Code)
3046 })
3050func TestAllowedRequestNegateWithMethod(t *testing.T) {
3051 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3052 w.WriteHeader(200)
3053 _, err := w.Write([]byte("Allowed Request"))
3054 if err != nil {
3055 t.Fatal(err)
3057 }))
3058 t.Cleanup(upstreamServer.Close)
3060 opts := baseTestOptions()
3061 opts.UpstreamServers = options.UpstreamConfig{
3062 Upstreams: []options.Upstream{
3064 ID: upstreamServer.URL,
3065 Path: "/",
3066 URI: upstreamServer.URL,
3067 },
3068 },
3070 opts.SkipAuthRoutes = []string{
3071 "GET!=^/api", // any non-api routes
3072 "POST=^/api/public-entity/?$",
3074 err := validation.Validate(opts)
3075 assert.NoError(t, err)
3076 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
3077 if err != nil {
3078 t.Fatal(err)
3081 testCases := []struct {
3082 name string
3083 method string
3084 url string
3085 allowed bool
3086 }{
3088 name: "Some static file allowed",
3089 method: http.MethodGet,
3090 url: "/static/file.txt",
3091 allowed: true,
3092 },
3094 name: "POST to contact form not allowed",
3095 method: http.MethodPost,
3096 url: "/contact",
3097 allowed: false,
3098 },
3100 name: "Regex POST allowed",
3101 method: http.MethodPost,
3102 url: "/api/public-entity",
3103 allowed: true,
3104 },
3106 name: "Regex POST with trailing slash allowed",
3107 method: http.MethodPost,
3108 url: "/api/public-entity/",
3109 allowed: true,
3110 },
3112 name: "Regex GET api route denied",
3113 method: http.MethodGet,
3114 url: "/api/users",
3115 allowed: false,
3116 },
3118 name: "Regex POST api route denied",
3119 method: http.MethodPost,
3120 url: "/api/users",
3121 allowed: false,
3122 },
3124 name: "Regex DELETE api route denied",
3125 method: "DELETE",
3126 url: "/api/users/1",
3127 allowed: false,
3128 },
3131 for _, tc := range testCases {
3132 t.Run(tc.name, func(t *testing.T) {
3133 req, err := http.NewRequest(tc.method, tc.url, nil)
3134 assert.NoError(t, err)
3135 assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req))
3137 rw := httptest.NewRecorder()
3138 proxy.ServeHTTP(rw, req)
3140 if tc.allowed {
3141 assert.Equal(t, 200, rw.Code)
3142 assert.Equal(t, "Allowed Request", rw.Body.String())
3143 } else {
3144 assert.Equal(t, 403, rw.Code)
3146 })
3150func TestProxyAllowedGroups(t *testing.T) {
3151 tests := []struct {
3152 name string
3153 allowedGroups []string
3154 groups []string
3155 querystring string
3156 expectUnauthorized bool
3157 }{
3159 name: "NoAllowedGroups",
3160 allowedGroups: []string{},
3161 groups: []string{},
3162 querystring: "",
3163 expectUnauthorized: false},
3165 name: "NoAllowedGroupsUserHasGroups",
3166 allowedGroups: []string{},
3167 groups: []string{"a", "b"},
3168 querystring: "",
3169 expectUnauthorized: false},
3171 name: "UserInAllowedGroup",
3172 allowedGroups: []string{"a"},
3173 groups: []string{"a", "b"},
3174 querystring: "",
3175 expectUnauthorized: false},
3177 name: "UserNotInAllowedGroup",
3178 allowedGroups: []string{"a"},
3179 groups: []string{"c"},
3180 querystring: "",
3181 expectUnauthorized: true},
3183 name: "UserInQuerystringGroup",
3184 allowedGroups: []string{"a", "b"},
3185 groups: []string{"a", "c"},
3186 querystring: "?allowed_groups=a",
3187 expectUnauthorized: false},
3189 name: "UserInMultiParamQuerystringGroup",
3190 allowedGroups: []string{"a", "b"},
3191 groups: []string{"b"},
3192 querystring: "?allowed_groups=a&allowed_groups=b,d",
3193 expectUnauthorized: false},
3195 name: "UserInOnlyQuerystringGroup",
3196 allowedGroups: []string{},
3197 groups: []string{"a", "c"},
3198 querystring: "?allowed_groups=a,b",
3199 expectUnauthorized: false},
3201 name: "UserInDelimitedQuerystringGroup",
3202 allowedGroups: []string{"a", "b", "c"},
3203 groups: []string{"c"},
3204 querystring: "?allowed_groups=a,c",
3205 expectUnauthorized: false},
3207 name: "UserNotInQuerystringGroup",
3208 allowedGroups: []string{},
3209 groups: []string{"c"},
3210 querystring: "?allowed_groups=a,b",
3211 expectUnauthorized: true},
3213 name: "UserInConfigGroupNotInQuerystringGroup",
3214 allowedGroups: []string{"a", "b", "c"},
3215 groups: []string{"c"},
3216 querystring: "?allowed_groups=a,b",
3217 expectUnauthorized: true},
3219 name: "UserInQuerystringGroupNotInConfigGroup",
3220 allowedGroups: []string{"a", "b"},
3221 groups: []string{"c"},
3222 querystring: "?allowed_groups=b,c",
3223 expectUnauthorized: true},
3226 for _, tt := range tests {
3227 t.Run(tt.name, func(t *testing.T) {
3228 emailAddress := "test"
3229 created := time.Now()
3231 session := &sessions.SessionState{
3232 Groups: tt.groups,
3233 Email: emailAddress,
3234 AccessToken: "oauth_token",
3235 CreatedAt: &created,
3238 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3239 w.WriteHeader(200)
3240 }))
3241 t.Cleanup(upstreamServer.Close)
3243 test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
3244 opts.Providers[0].AllowedGroups = tt.allowedGroups
3245 opts.UpstreamServers = options.UpstreamConfig{
3246 Upstreams: []options.Upstream{
3248 ID: upstreamServer.URL,
3249 Path: "/",
3250 URI: upstreamServer.URL,
3251 },
3252 },
3254 })
3255 if err != nil {
3256 t.Fatal(err)
3259 test.req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/%s", tt.querystring), nil)
3261 test.req.Header.Add("accept", applicationJSON)
3262 err = test.SaveSession(session)
3263 assert.NoError(t, err)
3264 test.proxy.ServeHTTP(test.rw, test.req)
3266 if tt.expectUnauthorized {
3267 assert.Equal(t, http.StatusForbidden, test.rw.Code)
3268 } else {
3269 assert.Equal(t, http.StatusOK, test.rw.Code)
3271 })
3275func TestAuthOnlyAllowedGroups(t *testing.T) {
3276 testCases := []struct {
3277 name string
3278 allowedGroups []string
3279 groups []string
3280 querystring string
3281 expectedStatusCode int
3282 }{
3284 name: "NoAllowedGroups",
3285 allowedGroups: []string{},
3286 groups: []string{},
3287 querystring: "",
3288 expectedStatusCode: http.StatusAccepted,
3289 },
3291 name: "NoAllowedGroupsUserHasGroups",
3292 allowedGroups: []string{},
3293 groups: []string{"a", "b"},
3294 querystring: "",
3295 expectedStatusCode: http.StatusAccepted,
3296 },
3298 name: "UserInAllowedGroup",
3299 allowedGroups: []string{"a"},
3300 groups: []string{"a", "b"},
3301 querystring: "",
3302 expectedStatusCode: http.StatusAccepted,
3303 },
3305 name: "UserNotInAllowedGroup",
3306 allowedGroups: []string{"a"},
3307 groups: []string{"c"},
3308 querystring: "",
3309 expectedStatusCode: http.StatusUnauthorized,
3310 },
3312 name: "UserInQuerystringGroup",
3313 allowedGroups: []string{"a", "b"},
3314 groups: []string{"a", "c"},
3315 querystring: "?allowed_groups=a",
3316 expectedStatusCode: http.StatusAccepted,
3317 },
3319 name: "UserInMultiParamQuerystringGroup",
3320 allowedGroups: []string{"a", "b"},
3321 groups: []string{"b"},
3322 querystring: "?allowed_groups=a&allowed_groups=b,d",
3323 expectedStatusCode: http.StatusAccepted,
3324 },
3326 name: "UserInOnlyQuerystringGroup",
3327 allowedGroups: []string{},
3328 groups: []string{"a", "c"},
3329 querystring: "?allowed_groups=a,b",
3330 expectedStatusCode: http.StatusAccepted,
3331 },
3333 name: "UserInDelimitedQuerystringGroup",
3334 allowedGroups: []string{"a", "b", "c"},
3335 groups: []string{"c"},
3336 querystring: "?allowed_groups=a,c",
3337 expectedStatusCode: http.StatusAccepted,
3338 },
3340 name: "UserNotInQuerystringGroup",
3341 allowedGroups: []string{},
3342 groups: []string{"c"},
3343 querystring: "?allowed_groups=a,b",
3344 expectedStatusCode: http.StatusForbidden,
3345 },
3347 name: "UserInConfigGroupNotInQuerystringGroup",
3348 allowedGroups: []string{"a", "b", "c"},
3349 groups: []string{"c"},
3350 querystring: "?allowed_groups=a,b",
3351 expectedStatusCode: http.StatusForbidden,
3352 },
3354 name: "UserInQuerystringGroupNotInConfigGroup",
3355 allowedGroups: []string{"a", "b"},
3356 groups: []string{"c"},
3357 querystring: "?allowed_groups=b,c",
3358 expectedStatusCode: http.StatusUnauthorized,
3359 },
3362 for _, tc := range testCases {
3363 t.Run(tc.name, func(t *testing.T) {
3364 emailAddress := "test"
3365 created := time.Now()
3367 session := &sessions.SessionState{
3368 Groups: tc.groups,
3369 Email: emailAddress,
3370 AccessToken: "oauth_token",
3371 CreatedAt: &created,
3374 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
3375 opts.Providers[0].AllowedGroups = tc.allowedGroups
3376 })
3377 if err != nil {
3378 t.Fatal(err)
3381 err = test.SaveSession(session)
3382 assert.NoError(t, err)
3384 test.proxy.ServeHTTP(test.rw, test.req)
3386 assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
3387 })
3391func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3392 testCases := []struct {
3393 name string
3394 groups []string
3395 method string
3396 ip string
3397 withSession bool
3398 expectedStatusCode int
3399 }{
3401 name: "UserWithGroupSkipAuthPreflight",
3402 groups: []string{"a", "c"},
3403 method: http.MethodOptions,
3404 ip: "1.2.3.5:43670",
3405 withSession: true,
3406 expectedStatusCode: http.StatusAccepted,
3407 },
3409 name: "UserWithGroupTrustedIp",
3410 groups: []string{"a", "c"},
3411 method: http.MethodGet,
3412 ip: "1.2.3.4:43670",
3413 withSession: true,
3414 expectedStatusCode: http.StatusAccepted,
3415 },
3417 name: "UserWithoutGroupSkipAuthPreflight",
3418 groups: []string{"c"},
3419 method: http.MethodOptions,
3420 ip: "1.2.3.5:43670",
3421 withSession: true,
3422 expectedStatusCode: http.StatusForbidden,
3423 },
3425 name: "UserWithoutGroupTrustedIp",
3426 groups: []string{"c"},
3427 method: http.MethodGet,
3428 ip: "1.2.3.4:43670",
3429 withSession: true,
3430 expectedStatusCode: http.StatusForbidden,
3431 },
3433 name: "UserWithoutSessionSkipAuthPreflight",
3434 method: http.MethodOptions,
3435 ip: "1.2.3.5:43670",
3436 withSession: false,
3437 expectedStatusCode: http.StatusAccepted,
3438 },
3440 name: "UserWithoutSessionTrustedIp",
3441 method: http.MethodGet,
3442 ip: "1.2.3.4:43670",
3443 withSession: false,
3444 expectedStatusCode: http.StatusAccepted,
3445 },
3448 for _, tc := range testCases {
3449 t.Run(tc.name, func(t *testing.T) {
3450 test, err := NewAuthOnlyEndpointTest("?allowed_groups=a,b", func(opts *options.Options) {
3451 opts.SkipAuthPreflight = true
3452 opts.TrustedIPs = []string{"1.2.3.4"}
3453 })
3454 if err != nil {
3455 t.Fatal(err)
3458 test.req.Method = tc.method
3459 test.req.RemoteAddr = tc.ip
3461 if tc.withSession {
3462 created := time.Now()
3463 session := &sessions.SessionState{
3464 Groups: tc.groups,
3465 Email: "test",
3466 AccessToken: "oauth_token",
3467 CreatedAt: &created,
3469 err = test.SaveSession(session)
3471 assert.NoError(t, err)
3473 test.proxy.ServeHTTP(test.rw, test.req)
3475 assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
3476 })
3480func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
3481 testCases := []struct {
3482 name string
3483 email string
3484 querystring string
3485 expectedStatusCode int
3486 }{
3488 name: "NotEmailRestriction",
3489 email: "toto@example.com",
3490 querystring: "",
3491 expectedStatusCode: http.StatusAccepted,
3492 },
3494 name: "UserInAllowedEmailDomain",
3495 email: "toto@example.com",
3496 querystring: "?allowed_email_domains=example.com",
3497 expectedStatusCode: http.StatusAccepted,
3498 },
3500 name: "UserNotInAllowedEmailDomain",
3501 email: "toto@example.com",
3502 querystring: "?allowed_email_domains=a.example.com",
3503 expectedStatusCode: http.StatusForbidden,
3504 },
3506 name: "UserNotInAllowedEmailDomains",
3507 email: "toto@example.com",
3508 querystring: "?allowed_email_domains=a.example.com,b.example.com",
3509 expectedStatusCode: http.StatusForbidden,
3510 },
3512 name: "UserInAllowedEmailDomains",
3513 email: "toto@example.com",
3514 querystring: "?allowed_email_domains=a.example.com,example.com",
3515 expectedStatusCode: http.StatusAccepted,
3516 },
3518 name: "UserInAllowedEmailDomainWildcard",
3519 email: "toto@foo.example.com",
3520 querystring: "?allowed_email_domains=*.example.com",
3521 expectedStatusCode: http.StatusAccepted,
3522 },
3524 name: "UserNotInAllowedEmailDomainWildcard",
3525 email: "toto@example.com",
3526 querystring: "?allowed_email_domains=*.a.example.com",
3527 expectedStatusCode: http.StatusForbidden,
3528 },
3530 name: "UserInAllowedEmailDomainsWildcard",
3531 email: "toto@example.com",
3532 querystring: "?allowed_email_domains=*.a.example.com,*.b.example.com",
3533 expectedStatusCode: http.StatusForbidden,
3534 },
3536 name: "UserInAllowedEmailDomainsWildcard",
3537 email: "toto@c.example.com",
3538 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
3539 expectedStatusCode: http.StatusAccepted,
3540 },
3542 name: "UserWithMultipleAtSignsExactDomain",
3543 email: "attacker@evil.com@example.com",
3544 querystring: "?allowed_email_domains=example.com",
3545 expectedStatusCode: http.StatusForbidden,
3546 },
3548 name: "UserWithMultipleAtSignsWildcardDomain",
3549 email: "attacker@evil.com@foo.example.com",
3550 querystring: "?allowed_email_domains=*.example.com",
3551 expectedStatusCode: http.StatusForbidden,
3552 },
3554 name: "UserWithMultipleAtSignsDotPrefixedDomain",
3555 email: "attacker@evil.com@foo.example.com",
3556 querystring: "?allowed_email_domains=.example.com",
3557 expectedStatusCode: http.StatusForbidden,
3558 },
3561 for _, tc := range testCases {
3562 t.Run(tc.name, func(t *testing.T) {
3563 groups := []string{}
3565 created := time.Now()
3567 session := &sessions.SessionState{
3568 Groups: groups,
3569 Email: tc.email,
3570 AccessToken: "oauth_token",
3571 CreatedAt: &created,
3574 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {})
3575 if err != nil {
3576 t.Fatal(err)
3579 err = test.SaveSession(session)
3580 assert.NoError(t, err)
3582 test.proxy.ServeHTTP(test.rw, test.req)
3584 assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
3585 })
3589func TestStateEncodesCorrectly(t *testing.T) {
3590 state := "some_state_to_test"
3591 nonce := "some_nonce_to_test"
3593 encodedResult := encodeState(nonce, state, true)
3594 assert.Equal(t, "c29tZV9ub25jZV90b190ZXN0OnNvbWVfc3RhdGVfdG9fdGVzdA", encodedResult)
3596 notEncodedResult := encodeState(nonce, state, false)
3597 assert.Equal(t, "some_nonce_to_test:some_state_to_test", notEncodedResult)
3600func TestStateDecodesCorrectly(t *testing.T) {
3601 nonce, redirect, _ := decodeState("c29tZV9ub25jZV90b190ZXN0OnNvbWVfc3RhdGVfdG9fdGVzdA", true)
3603 assert.Equal(t, "some_nonce_to_test", nonce)
3604 assert.Equal(t, "some_state_to_test", redirect)
3606 nonce2, redirect2, _ := decodeState("some_nonce_to_test:some_state_to_test", false)
3608 assert.Equal(t, "some_nonce_to_test", nonce2)
3609 assert.Equal(t, "some_state_to_test", redirect2)
3612func TestAuthOnlyAllowedEmails(t *testing.T) {
3613 testCases := []struct {
3614 name string
3615 email string
3616 querystring string
3617 expectedStatusCode int
3618 }{
3620 name: "NotEmailRestriction",
3621 email: "toto@example.com",
3622 querystring: "",
3623 expectedStatusCode: http.StatusAccepted,
3624 },
3626 name: "UserInAllowedEmail",
3627 email: "toto@example.com",
3628 querystring: "?allowed_emails=toto@example.com",
3629 expectedStatusCode: http.StatusAccepted,
3630 },
3632 name: "UserNotInAllowedEmail",
3633 email: "toto@example.com",
3634 querystring: "?allowed_emails=tete@example.com",
3635 expectedStatusCode: http.StatusForbidden,
3636 },
3638 name: "UserNotInAllowedEmails",
3639 email: "toto@example.com",
3640 querystring: "?allowed_emails=tete@example.com,tutu@example.com",
3641 expectedStatusCode: http.StatusForbidden,
3642 },
3644 name: "UserInAllowedEmails",
3645 email: "toto@example.com",
3646 querystring: "?allowed_emails=tete@example.com,toto@example.com",
3647 expectedStatusCode: http.StatusAccepted,
3648 },
3651 for _, tc := range testCases {
3652 t.Run(tc.name, func(t *testing.T) {
3653 groups := []string{}
3655 created := time.Now()
3657 session := &sessions.SessionState{
3658 Groups: groups,
3659 Email: tc.email,
3660 AccessToken: "oauth_token",
3661 CreatedAt: &created,
3664 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {})
3665 if err != nil {
3666 t.Fatal(err)
3669 err = test.SaveSession(session)
3670 assert.NoError(t, err)
3672 test.proxy.ServeHTTP(test.rw, test.req)
3674 assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
3675 })
3679func TestGetOAuthRedirectURI(t *testing.T) {
3680 tests := []struct {
3681 name string
3682 setupOpts func(*options.Options) *options.Options
3683 req *http.Request
3684 want string
3685 }{
3687 name: "redirect with https schema",
3688 setupOpts: func(baseOpts *options.Options) *options.Options {
3689 return baseOpts
3690 },
3691 req: &http.Request{
3692 Host: "example",
3693 URL: &url.URL{
3694 Scheme: schemeHTTPS,
3695 },
3696 },
3697 want: "https://example/oauth2/callback",
3698 },
3700 name: "redirect with http schema",
3701 setupOpts: func(baseOpts *options.Options) *options.Options {
3702 baseOpts.Cookie.Secure = false
3703 return baseOpts
3704 },
3705 req: &http.Request{
3706 Host: "example",
3707 URL: &url.URL{
3708 Scheme: schemeHTTP,
3709 },
3710 },
3711 want: "http://example/oauth2/callback",
3712 },
3714 name: "relative redirect url",
3715 setupOpts: func(baseOpts *options.Options) *options.Options {
3716 baseOpts.RelativeRedirectURL = true
3717 return baseOpts
3718 },
3719 req: &http.Request{},
3720 want: "/oauth2/callback",
3721 },
3723 name: "proxy prefix",
3724 setupOpts: func(baseOpts *options.Options) *options.Options {
3725 baseOpts.ProxyPrefix = "/prefix"
3726 return baseOpts
3727 },
3728 req: &http.Request{
3729 Host: "example",
3730 URL: &url.URL{
3731 Scheme: schemeHTTP,
3732 },
3733 },
3734 want: "https://example/prefix/callback",
3735 },
3737 name: "proxy prefix with relative redirect",
3738 setupOpts: func(baseOpts *options.Options) *options.Options {
3739 baseOpts.ProxyPrefix = "/prefix"
3740 baseOpts.RelativeRedirectURL = true
3741 return baseOpts
3742 },
3743 req: &http.Request{
3744 Host: "example",
3745 URL: &url.URL{
3746 Scheme: schemeHTTP,
3747 },
3748 },
3749 want: "/prefix/callback",
3750 },
3752 for _, tt := range tests {
3753 t.Run(tt.name, func(t *testing.T) {
3754 baseOpts := baseTestOptions()
3755 err := validation.Validate(baseOpts)
3756 assert.NoError(t, err)
3758 proxy, err := NewOAuthProxy(tt.setupOpts(baseOpts), func(string) bool { return true })
3759 assert.NoError(t, err)
3761 assert.Equalf(t, tt.want, proxy.getOAuthRedirectURI(tt.req), "getOAuthRedirectURI(%v)", tt.req)
3762 })
3766func TestIdTokenPlaceholderInSignOut(t *testing.T) {
3767 opts := baseTestOptions()
3768 opts.WhitelistDomains = []string{"my-oidc-provider.example.com"}
3770 err := validation.Validate(opts)
3771 assert.NoError(t, err)
3773 const emailAddress = "john.doe@example.com"
3774 const userName = "9fcab5c9b889a557"
3775 created := time.Now()
3777 session := &sessions.SessionState{
3778 User: userName,
3779 Groups: []string{"a", "b"},
3780 Email: emailAddress,
3781 IDToken: "eYjjjjjj.vvvv.ddd",
3782 AccessToken: "oauth_token",
3783 CreatedAt: &created,
3786 proxy, err := NewOAuthProxy(opts, func(email string) bool {
3787 return true
3788 })
3789 assert.NoError(t, err)
3791 // Save the required session
3792 rw := httptest.NewRecorder()
3793 req, _ := http.NewRequest(http.MethodGet, "/", nil)
3794 err = proxy.sessionStore.Save(rw, req, session)
3795 assert.NoError(t, err)
3797 rw = httptest.NewRecorder()
3799 rdUrl := url.QueryEscape("https://my-oidc-provider.example.com/sign_out_page?id_token_hint={id_token}&post_logout_redirect_uri=https://my-app.example.com/")
3800 req, _ = http.NewRequest(http.MethodGet, "/oauth2/sign_out?rd="+rdUrl, nil)
3801 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
3802 RequestID: "11111111-2222-4333-8444-555555555555",
3803 Session: session,
3804 })
3806 proxy.SignOut(rw, req)
3807 newLocation := rw.Header().Values("Location")[0]
3809 assert.Equal(t, "https://my-oidc-provider.example.com/sign_out_page?id_token_hint=eYjjjjjj.vvvv.ddd&post_logout_redirect_uri=https://my-app.example.com/", newLocation)