git.iko.soy/typeable/oauth2-proxy
on 86e672e help sign in
typeable/oauth2-proxy/oauthproxy_test.go
2941 lines 77 kB history raw
1package main
3import (
4 "bufio"
5 "context"
6 "crypto"
7 "encoding/base64"
8 "fmt"
9 "io"
10 "io/ioutil"
11 "net/http"
12 "net/http/httptest"
13 "net/url"
14 "os"
15 "regexp"
16 "strings"
17 "testing"
18 "time"
20 "github.com/coreos/go-oidc"
21 "github.com/mbland/hmacauth"
22 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
23 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
24 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
25 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
26 sessionscookie "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
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("GET", "/robots.txt", nil)
58 proxy.ServeHTTP(rw, req)
59 assert.Equal(t, 200, rw.Code)
60 assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String())
63func TestIsValidRedirect(t *testing.T) {
64 opts := baseTestOptions()
65 // Should match domains that are exactly foo.bar and any subdomain of bar.foo
66 opts.WhitelistDomains = []string{
67 "foo.bar",
68 ".bar.foo",
69 "port.bar:8080",
70 ".sub.port.bar:8080",
71 "anyport.bar:*",
72 ".sub.anyport.bar:*",
73 }
74 err := validation.Validate(opts)
75 assert.NoError(t, err)
77 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
78 if err != nil {
79 t.Fatal(err)
80 }
82 testCases := []struct {
83 Desc, Redirect string
84 ExpectedResult bool
85 }{
86 {
87 Desc: "noRD",
88 Redirect: "",
89 ExpectedResult: false,
90 },
91 {
92 Desc: "singleSlash",
93 Redirect: "/redirect",
94 ExpectedResult: true,
95 },
96 {
97 Desc: "doubleSlash",
98 Redirect: "//redirect",
99 ExpectedResult: false,
100 },
101 {
102 Desc: "validHTTP",
103 Redirect: "http://foo.bar/redirect",
104 ExpectedResult: true,
105 },
106 {
107 Desc: "validHTTPS",
108 Redirect: "https://foo.bar/redirect",
109 ExpectedResult: true,
110 },
111 {
112 Desc: "invalidHTTPSubdomain",
113 Redirect: "http://baz.foo.bar/redirect",
114 ExpectedResult: false,
115 },
116 {
117 Desc: "invalidHTTPSSubdomain",
118 Redirect: "https://baz.foo.bar/redirect",
119 ExpectedResult: false,
120 },
121 {
122 Desc: "validHTTPSubdomain",
123 Redirect: "http://baz.bar.foo/redirect",
124 ExpectedResult: true,
125 },
126 {
127 Desc: "validHTTPSSubdomain",
128 Redirect: "https://baz.bar.foo/redirect",
129 ExpectedResult: true,
130 },
131 {
132 Desc: "validHTTPDomain",
133 Redirect: "http://bar.foo/redirect",
134 ExpectedResult: true,
135 },
136 {
137 Desc: "invalidHTTP1",
138 Redirect: "http://foo.bar.evil.corp/redirect",
139 ExpectedResult: false,
140 },
141 {
142 Desc: "invalidHTTPS1",
143 Redirect: "https://foo.bar.evil.corp/redirect",
144 ExpectedResult: false,
145 },
146 {
147 Desc: "invalidHTTP2",
148 Redirect: "http://evil.corp/redirect?rd=foo.bar",
149 ExpectedResult: false,
150 },
151 {
152 Desc: "invalidHTTPS2",
153 Redirect: "https://evil.corp/redirect?rd=foo.bar",
154 ExpectedResult: false,
155 },
156 {
157 Desc: "invalidPort",
158 Redirect: "https://evil.corp:3838/redirect",
159 ExpectedResult: false,
160 },
161 {
162 Desc: "invalidEmptyPort",
163 Redirect: "http://foo.bar:3838/redirect",
164 ExpectedResult: false,
165 },
166 {
167 Desc: "invalidEmptyPortSubdomain",
168 Redirect: "http://baz.bar.foo:3838/redirect",
169 ExpectedResult: false,
170 },
171 {
172 Desc: "validSpecificPort",
173 Redirect: "http://port.bar:8080/redirect",
174 ExpectedResult: true,
175 },
176 {
177 Desc: "invalidSpecificPort",
178 Redirect: "http://port.bar:3838/redirect",
179 ExpectedResult: false,
180 },
181 {
182 Desc: "validSpecificPortSubdomain",
183 Redirect: "http://foo.sub.port.bar:8080/redirect",
184 ExpectedResult: true,
185 },
186 {
187 Desc: "invalidSpecificPortSubdomain",
188 Redirect: "http://foo.sub.port.bar:3838/redirect",
189 ExpectedResult: false,
190 },
191 {
192 Desc: "validAnyPort1",
193 Redirect: "http://anyport.bar:8080/redirect",
194 ExpectedResult: true,
195 },
196 {
197 Desc: "validAnyPort2",
198 Redirect: "http://anyport.bar:8081/redirect",
199 ExpectedResult: true,
200 },
201 {
202 Desc: "validAnyPortSubdomain1",
203 Redirect: "http://a.sub.anyport.bar:8080/redirect",
204 ExpectedResult: true,
205 },
206 {
207 Desc: "validAnyPortSubdomain2",
208 Redirect: "http://a.sub.anyport.bar:8081/redirect",
209 ExpectedResult: true,
210 },
211 {
212 Desc: "openRedirect1",
213 Redirect: "/\\evil.com",
214 ExpectedResult: false,
215 },
216 {
217 Desc: "openRedirectSpace1",
218 Redirect: "/ /evil.com",
219 ExpectedResult: false,
220 },
221 {
222 Desc: "openRedirectSpace2",
223 Redirect: "/ \\evil.com",
224 ExpectedResult: false,
225 },
226 {
227 Desc: "openRedirectTab1",
228 Redirect: "/\t/evil.com",
229 ExpectedResult: false,
230 },
231 {
232 Desc: "openRedirectTab2",
233 Redirect: "/\t\\evil.com",
234 ExpectedResult: false,
235 },
236 {
237 Desc: "openRedirectVerticalTab1",
238 Redirect: "/\v/evil.com",
239 ExpectedResult: false,
240 },
241 {
242 Desc: "openRedirectVerticalTab2",
243 Redirect: "/\v\\evil.com",
244 ExpectedResult: false,
245 },
246 {
247 Desc: "openRedirectNewLine1",
248 Redirect: "/\n/evil.com",
249 ExpectedResult: false,
250 },
251 {
252 Desc: "openRedirectNewLine2",
253 Redirect: "/\n\\evil.com",
254 ExpectedResult: false,
255 },
256 {
257 Desc: "openRedirectCarriageReturn1",
258 Redirect: "/\r/evil.com",
259 ExpectedResult: false,
260 },
261 {
262 Desc: "openRedirectCarriageReturn2",
263 Redirect: "/\r\\evil.com",
264 ExpectedResult: false,
265 },
266 {
267 Desc: "openRedirectTripleTab",
268 Redirect: "/\t\t/\t/evil.com",
269 ExpectedResult: false,
270 },
271 {
272 Desc: "openRedirectTripleTab2",
273 Redirect: "/\t\t\\\t/evil.com",
274 ExpectedResult: false,
275 },
276 {
277 Desc: "openRedirectQuadTab1",
278 Redirect: "/\t\t/\t\t\\evil.com",
279 ExpectedResult: false,
280 },
281 {
282 Desc: "openRedirectQuadTab2",
283 Redirect: "/\t\t\\\t\t/evil.com",
284 ExpectedResult: false,
285 },
286 {
287 Desc: "openRedirectPeriod1",
288 Redirect: "/./\\evil.com",
289 ExpectedResult: false,
290 },
291 {
292 Desc: "openRedirectPeriod2",
293 Redirect: "/./../../\\evil.com",
294 ExpectedResult: false,
295 },
296 {
297 Desc: "openRedirectDoubleTab",
298 Redirect: "/\t/\t\\evil.com",
299 ExpectedResult: false,
300 },
301 {
302 Desc: "openRedirectPartialSubdomain",
303 Redirect: "http://evilbar.foo",
304 ExpectedResult: false,
305 },
306 }
308 for _, tc := range testCases {
309 t.Run(tc.Desc, func(t *testing.T) {
310 result := proxy.IsValidRedirect(tc.Redirect)
312 if result != tc.ExpectedResult {
313 t.Errorf("expected %t got %t", tc.ExpectedResult, result)
314 }
315 })
316 }
319func TestOpenRedirects(t *testing.T) {
320 opts := baseTestOptions()
321 // Should match domains that are exactly foo.bar and any subdomain of bar.foo
322 opts.WhitelistDomains = []string{
323 "foo.bar",
324 ".bar.foo",
325 "port.bar:8080",
326 ".sub.port.bar:8080",
327 "anyport.bar:*",
328 ".sub.anyport.bar:*",
329 "www.whitelisteddomain.tld",
330 }
331 err := validation.Validate(opts)
332 assert.NoError(t, err)
334 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
335 if err != nil {
336 t.Fatal(err)
337 }
339 file, err := os.Open("./testdata/openredirects.txt")
340 if err != nil {
341 t.Fatal(err)
342 }
343 defer func(t *testing.T) {
344 if err := file.Close(); err != nil {
345 t.Fatal(err)
346 }
347 }(t)
349 scanner := bufio.NewScanner(file)
350 for scanner.Scan() {
351 rd := scanner.Text()
352 t.Run(rd, func(t *testing.T) {
353 rdUnescaped, err := url.QueryUnescape(rd)
354 if err != nil {
355 t.Fatal(err)
356 }
357 if proxy.IsValidRedirect(rdUnescaped) {
358 t.Errorf("Expected %q to not be valid (unescaped: %q)", rd, rdUnescaped)
359 }
360 })
361 }
363 if err := scanner.Err(); err != nil {
364 t.Fatal(err)
365 }
368type TestProvider struct {
369 *providers.ProviderData
370 EmailAddress string
371 ValidToken bool
372 GroupValidator func(string) bool
375var _ providers.Provider = (*TestProvider)(nil)
377func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
378 return &TestProvider{
379 ProviderData: &providers.ProviderData{
380 ProviderName: "Test Provider",
381 LoginURL: &url.URL{
382 Scheme: "http",
383 Host: providerURL.Host,
384 Path: "/oauth/authorize",
385 },
386 RedeemURL: &url.URL{
387 Scheme: "http",
388 Host: providerURL.Host,
389 Path: "/oauth/token",
390 },
391 ProfileURL: &url.URL{
392 Scheme: "http",
393 Host: providerURL.Host,
394 Path: "/api/v1/profile",
395 },
396 Scope: "profile.email",
397 },
398 EmailAddress: emailAddress,
399 GroupValidator: func(s string) bool {
400 return true
401 },
402 }
405func (tp *TestProvider) GetEmailAddress(_ context.Context, _ *sessions.SessionState) (string, error) {
406 return tp.EmailAddress, nil
409func (tp *TestProvider) ValidateSession(_ context.Context, _ *sessions.SessionState) bool {
410 return tp.ValidToken
413func Test_redeemCode(t *testing.T) {
414 opts := baseTestOptions()
415 err := validation.Validate(opts)
416 assert.NoError(t, err)
418 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
419 if err != nil {
420 t.Fatal(err)
421 }
423 req := httptest.NewRequest(http.MethodGet, "/", nil)
424 _, err = proxy.redeemCode(req)
425 assert.Equal(t, providers.ErrMissingCode, err)
428func Test_enrichSession(t *testing.T) {
429 const (
430 sessionUser = "Mr Session"
431 sessionEmail = "session@example.com"
432 providerEmail = "provider@example.com"
433 )
435 testCases := map[string]struct {
436 session *sessions.SessionState
437 expectedUser string
438 expectedEmail string
439 }{
440 "Session already has enrichable fields": {
441 session: &sessions.SessionState{
442 User: sessionUser,
443 Email: sessionEmail,
444 },
445 expectedUser: sessionUser,
446 expectedEmail: sessionEmail,
447 },
448 "Session is missing Email and GetEmailAddress is implemented": {
449 session: &sessions.SessionState{
450 User: sessionUser,
451 },
452 expectedUser: sessionUser,
453 expectedEmail: providerEmail,
454 },
455 "Session is missing User and GetUserName is not implemented": {
456 session: &sessions.SessionState{
457 Email: sessionEmail,
458 },
459 expectedUser: "",
460 expectedEmail: sessionEmail,
461 },
462 }
464 for name, tc := range testCases {
465 t.Run(name, func(t *testing.T) {
466 opts := baseTestOptions()
467 err := validation.Validate(opts)
468 assert.NoError(t, err)
470 // intentionally set after validation.Validate(opts) since it will clobber
471 // our TestProvider and call `providers.New` defaulting to `providers.GoogleProvider`
472 opts.SetProvider(NewTestProvider(&url.URL{Host: "www.example.com"}, providerEmail))
473 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
474 if err != nil {
475 t.Fatal(err)
476 }
478 err = proxy.enrichSessionState(context.Background(), tc.session)
479 assert.NoError(t, err)
480 assert.Equal(t, tc.expectedUser, tc.session.User)
481 assert.Equal(t, tc.expectedEmail, tc.session.Email)
482 })
483 }
486func TestBasicAuthPassword(t *testing.T) {
487 providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
488 logger.Printf("%#v", r)
489 var payload string
490 switch r.URL.Path {
491 case "/oauth/token":
492 payload = `{"access_token": "my_auth_token"}`
493 default:
494 payload = r.Header.Get("Authorization")
495 if payload == "" {
496 payload = "No Authorization header found."
497 }
498 }
499 w.WriteHeader(200)
500 _, err := w.Write([]byte(payload))
501 if err != nil {
502 t.Fatal(err)
503 }
504 }))
506 basicAuthPassword := "This is a secure password"
507 opts := baseTestOptions()
508 opts.UpstreamServers = options.Upstreams{
509 {
510 ID: providerServer.URL,
511 Path: "/",
512 URI: providerServer.URL,
513 },
514 }
516 opts.Cookie.Secure = false
517 opts.InjectRequestHeaders = []options.Header{
518 {
519 Name: "Authorization",
520 Values: []options.HeaderValue{
521 {
522 ClaimSource: &options.ClaimSource{
523 Claim: "email",
524 BasicAuthPassword: &options.SecretSource{
525 Value: []byte(basicAuthPassword),
526 },
527 },
528 },
529 },
530 },
531 }
533 err := validation.Validate(opts)
534 assert.NoError(t, err)
536 providerURL, _ := url.Parse(providerServer.URL)
537 const emailAddress = "john.doe@example.com"
539 opts.SetProvider(NewTestProvider(providerURL, emailAddress))
540 proxy, err := NewOAuthProxy(opts, func(email string) bool {
541 return email == emailAddress
542 })
543 if err != nil {
544 t.Fatal(err)
545 }
547 // Save the required session
548 rw := httptest.NewRecorder()
549 req, _ := http.NewRequest("GET", "/", nil)
550 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
551 Email: emailAddress,
552 })
553 assert.NoError(t, err)
555 // Extract the cookie value to inject into the test request
556 cookie := rw.Header().Values("Set-Cookie")[0]
558 req, _ = http.NewRequest("GET", "/", nil)
559 req.Header.Set("Cookie", cookie)
560 rw = httptest.NewRecorder()
561 proxy.ServeHTTP(rw, req)
563 // The username in the basic auth credentials is expected to be equal to the email address from the
564 // auth response, so we use the same variable here.
565 expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(emailAddress+":"+basicAuthPassword))
566 assert.Equal(t, expectedHeader, rw.Body.String())
567 providerServer.Close()
570func TestPassGroupsHeadersWithGroups(t *testing.T) {
571 opts := baseTestOptions()
572 opts.InjectRequestHeaders = []options.Header{
573 {
574 Name: "X-Forwarded-Groups",
575 Values: []options.HeaderValue{
576 {
577 ClaimSource: &options.ClaimSource{
578 Claim: "groups",
579 },
580 },
581 },
582 },
583 }
585 err := validation.Validate(opts)
586 assert.NoError(t, err)
588 const emailAddress = "john.doe@example.com"
589 const userName = "9fcab5c9b889a557"
591 groups := []string{"a", "b"}
592 created := time.Now()
593 session := &sessions.SessionState{
594 User: userName,
595 Groups: groups,
596 Email: emailAddress,
597 AccessToken: "oauth_token",
598 CreatedAt: &created,
599 }
601 proxy, err := NewOAuthProxy(opts, func(email string) bool {
602 return email == emailAddress
603 })
604 assert.NoError(t, err)
606 // Save the required session
607 rw := httptest.NewRecorder()
608 req, _ := http.NewRequest("GET", "/", nil)
609 err = proxy.sessionStore.Save(rw, req, session)
610 assert.NoError(t, err)
612 // Extract the cookie value to inject into the test request
613 cookie := rw.Header().Values("Set-Cookie")[0]
615 req, _ = http.NewRequest("GET", "/", nil)
616 req.Header.Set("Cookie", cookie)
617 rw = httptest.NewRecorder()
618 proxy.ServeHTTP(rw, req)
620 assert.Equal(t, []string{"a,b"}, req.Header["X-Forwarded-Groups"])
623type PassAccessTokenTest struct {
624 providerServer *httptest.Server
625 proxy *OAuthProxy
626 opts *options.Options
629type PassAccessTokenTestOptions struct {
630 PassAccessToken bool
631 ProxyUpstream options.Upstream
634func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) (*PassAccessTokenTest, error) {
635 patt := &PassAccessTokenTest{}
637 patt.providerServer = httptest.NewServer(
638 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
639 var payload string
640 switch r.URL.Path {
641 case "/oauth/token":
642 payload = `{"access_token": "my_auth_token"}`
643 default:
644 payload = r.Header.Get("X-Forwarded-Access-Token")
645 if payload == "" {
646 payload = "No access token found."
647 }
648 }
649 w.WriteHeader(200)
650 _, err := w.Write([]byte(payload))
651 if err != nil {
652 panic(err)
653 }
654 }))
656 patt.opts = baseTestOptions()
657 patt.opts.UpstreamServers = options.Upstreams{
658 {
659 ID: patt.providerServer.URL,
660 Path: "/",
661 URI: patt.providerServer.URL,
662 },
663 }
664 if opts.ProxyUpstream.ID != "" {
665 patt.opts.UpstreamServers = append(patt.opts.UpstreamServers, opts.ProxyUpstream)
666 }
668 patt.opts.Cookie.Secure = false
669 if opts.PassAccessToken {
670 patt.opts.InjectRequestHeaders = []options.Header{
671 {
672 Name: "X-Forwarded-Access-Token",
673 Values: []options.HeaderValue{
674 {
675 ClaimSource: &options.ClaimSource{
676 Claim: "access_token",
677 },
678 },
679 },
680 },
681 }
682 }
684 err := validation.Validate(patt.opts)
685 if err != nil {
686 return nil, err
687 }
689 providerURL, _ := url.Parse(patt.providerServer.URL)
690 const emailAddress = "michael.bland@gsa.gov"
692 patt.opts.SetProvider(NewTestProvider(providerURL, emailAddress))
693 patt.proxy, err = NewOAuthProxy(patt.opts, func(email string) bool {
694 return email == emailAddress
695 })
696 if err != nil {
697 return nil, err
698 }
699 return patt, nil
702func (patTest *PassAccessTokenTest) Close() {
703 patTest.providerServer.Close()
706func (patTest *PassAccessTokenTest) getCallbackEndpoint() (httpCode int,
707 cookie string) {
708 rw := httptest.NewRecorder()
709 req, err := http.NewRequest("GET", "/oauth2/callback?code=callback_code&state=nonce:",
710 strings.NewReader(""))
711 if err != nil {
712 return 0, ""
713 }
714 req.AddCookie(patTest.proxy.MakeCSRFCookie(req, "nonce", time.Hour, time.Now()))
715 patTest.proxy.ServeHTTP(rw, req)
716 return rw.Code, rw.Header().Values("Set-Cookie")[1]
719// getEndpointWithCookie makes a requests againt the oauthproxy with passed requestPath
720// and cookie and returns body and status code.
721func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoint string) (httpCode int, accessToken string) {
722 cookieName := patTest.proxy.CookieName
723 var value string
724 keyPrefix := cookieName + "="
726 for _, field := range strings.Split(cookie, "; ") {
727 value = strings.TrimPrefix(field, keyPrefix)
728 if value != field {
729 break
730 } else {
731 value = ""
732 }
733 }
734 if value == "" {
735 return 0, ""
736 }
738 req, err := http.NewRequest("GET", endpoint, strings.NewReader(""))
739 if err != nil {
740 return 0, ""
741 }
742 req.AddCookie(&http.Cookie{
743 Name: cookieName,
744 Value: value,
745 Path: "/",
746 Expires: time.Now().Add(time.Duration(24)),
747 HttpOnly: true,
748 })
750 rw := httptest.NewRecorder()
751 patTest.proxy.ServeHTTP(rw, req)
752 return rw.Code, rw.Body.String()
755func TestForwardAccessTokenUpstream(t *testing.T) {
756 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
757 PassAccessToken: true,
758 })
759 if err != nil {
760 t.Fatal(err)
761 }
762 t.Cleanup(patTest.Close)
764 // A successful validation will redirect and set the auth cookie.
765 code, cookie := patTest.getCallbackEndpoint()
766 if code != 302 {
767 t.Fatalf("expected 302; got %d", code)
768 }
769 assert.NotNil(t, cookie)
771 // Now we make a regular request; the access_token from the cookie is
772 // forwarded as the "X-Forwarded-Access-Token" header. The token is
773 // read by the test provider server and written in the response body.
774 code, payload := patTest.getEndpointWithCookie(cookie, "/")
775 if code != 200 {
776 t.Fatalf("expected 200; got %d", code)
777 }
778 assert.Equal(t, "my_auth_token", payload)
781func TestStaticProxyUpstream(t *testing.T) {
782 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
783 PassAccessToken: true,
784 ProxyUpstream: options.Upstream{
785 ID: "static-proxy",
786 Path: "/static-proxy",
787 Static: true,
788 },
789 })
790 if err != nil {
791 t.Fatal(err)
792 }
793 t.Cleanup(patTest.Close)
795 // A successful validation will redirect and set the auth cookie.
796 code, cookie := patTest.getCallbackEndpoint()
797 if code != 302 {
798 t.Fatalf("expected 302; got %d", code)
799 }
800 assert.NotEqual(t, nil, cookie)
802 // Now we make a regular request against the upstream proxy; And validate
803 // the returned status code through the static proxy.
804 code, payload := patTest.getEndpointWithCookie(cookie, "/static-proxy")
805 if code != 200 {
806 t.Fatalf("expected 200; got %d", code)
807 }
808 assert.Equal(t, "Authenticated", payload)
811func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
812 patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
813 PassAccessToken: false,
814 })
815 if err != nil {
816 t.Fatal(err)
817 }
818 t.Cleanup(patTest.Close)
820 // A successful validation will redirect and set the auth cookie.
821 code, cookie := patTest.getCallbackEndpoint()
822 if code != 302 {
823 t.Fatalf("expected 302; got %d", code)
824 }
825 assert.NotEqual(t, nil, cookie)
827 // Now we make a regular request, but the access token header should
828 // not be present.
829 code, payload := patTest.getEndpointWithCookie(cookie, "/")
830 if code != 200 {
831 t.Fatalf("expected 200; got %d", code)
832 }
833 assert.Equal(t, "No access token found.", payload)
836type SignInPageTest struct {
837 opts *options.Options
838 proxy *OAuthProxy
839 signInRegexp *regexp.Regexp
840 signInProviderRegexp *regexp.Regexp
843const signInRedirectPattern = `<input type="hidden" name="rd" value="(.*)">`
844const signInSkipProvider = `>Found<`
846func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
847 var sipTest SignInPageTest
849 sipTest.opts = baseTestOptions()
850 sipTest.opts.SkipProviderButton = skipProvider
851 err := validation.Validate(sipTest.opts)
852 if err != nil {
853 return nil, err
854 }
856 sipTest.proxy, err = NewOAuthProxy(sipTest.opts, func(email string) bool {
857 return true
858 })
859 if err != nil {
860 return nil, err
861 }
862 sipTest.signInRegexp = regexp.MustCompile(signInRedirectPattern)
863 sipTest.signInProviderRegexp = regexp.MustCompile(signInSkipProvider)
865 return &sipTest, nil
868func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
869 rw := httptest.NewRecorder()
870 req, _ := http.NewRequest("GET", endpoint, strings.NewReader(""))
871 sipTest.proxy.ServeHTTP(rw, req)
872 return rw.Code, rw.Body.String()
875func TestSignInPageIncludesTargetRedirect(t *testing.T) {
876 sipTest, err := NewSignInPageTest(false)
877 if err != nil {
878 t.Fatal(err)
879 }
880 const endpoint = "/some/random/endpoint"
882 code, body := sipTest.GetEndpoint(endpoint)
883 assert.Equal(t, 403, code)
885 match := sipTest.signInRegexp.FindStringSubmatch(body)
886 if match == nil {
887 t.Fatal("Did not find pattern in body: " +
888 signInRedirectPattern + "\nBody:\n" + body)
889 }
890 if match[1] != endpoint {
891 t.Fatal(`expected redirect to "` + endpoint +
892 `", but was "` + match[1] + `"`)
893 }
896func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) {
897 sipTest, err := NewSignInPageTest(false)
898 if err != nil {
899 t.Fatal(err)
900 }
901 code, body := sipTest.GetEndpoint("/oauth2/sign_in")
902 assert.Equal(t, 200, code)
904 match := sipTest.signInRegexp.FindStringSubmatch(body)
905 if match == nil {
906 t.Fatal("Did not find pattern in body: " +
907 signInRedirectPattern + "\nBody:\n" + body)
908 }
909 if match[1] != "/" {
910 t.Fatal(`expected redirect to "/", but was "` + match[1] + `"`)
911 }
914func TestSignInPageSkipProvider(t *testing.T) {
915 sipTest, err := NewSignInPageTest(true)
916 if err != nil {
917 t.Fatal(err)
918 }
920 endpoint := "/some/random/endpoint"
922 code, body := sipTest.GetEndpoint(endpoint)
923 assert.Equal(t, 302, code)
925 match := sipTest.signInProviderRegexp.FindStringSubmatch(body)
926 if match == nil {
927 t.Fatal("Did not find pattern in body: " +
928 signInSkipProvider + "\nBody:\n" + body)
929 }
932func TestSignInPageSkipProviderDirect(t *testing.T) {
933 sipTest, err := NewSignInPageTest(true)
934 if err != nil {
935 t.Fatal(err)
936 }
938 endpoint := "/sign_in"
940 code, body := sipTest.GetEndpoint(endpoint)
941 assert.Equal(t, 302, code)
943 match := sipTest.signInProviderRegexp.FindStringSubmatch(body)
944 if match == nil {
945 t.Fatal("Did not find pattern in body: " +
946 signInSkipProvider + "\nBody:\n" + body)
947 }
950type ProcessCookieTest struct {
951 opts *options.Options
952 proxy *OAuthProxy
953 rw *httptest.ResponseRecorder
954 req *http.Request
955 validateUser bool
958type ProcessCookieTestOpts struct {
959 providerValidateCookieResponse bool
962type OptionsModifier func(*options.Options)
964func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
965 var pcTest ProcessCookieTest
967 pcTest.opts = baseTestOptions()
968 for _, modifier := range modifiers {
969 modifier(pcTest.opts)
970 }
971 // First, set the CookieRefresh option so proxy.AesCipher is created,
972 // needed to encrypt the access_token.
973 pcTest.opts.Cookie.Refresh = time.Hour
974 err := validation.Validate(pcTest.opts)
975 if err != nil {
976 return nil, err
977 }
979 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
980 return pcTest.validateUser
981 })
982 if err != nil {
983 return nil, err
984 }
985 pcTest.proxy.provider = &TestProvider{
986 ProviderData: &providers.ProviderData{},
987 ValidToken: opts.providerValidateCookieResponse,
988 }
989 pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.AllowedGroups)
991 // Now, zero-out proxy.CookieRefresh for the cases that don't involve
992 // access_token validation.
993 pcTest.proxy.CookieRefresh = time.Duration(0)
994 pcTest.rw = httptest.NewRecorder()
995 pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
996 pcTest.validateUser = true
997 return &pcTest, nil
1000func NewProcessCookieTestWithDefaults() (*ProcessCookieTest, error) {
1001 return NewProcessCookieTest(ProcessCookieTestOpts{
1002 providerValidateCookieResponse: true,
1003 })
1006func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1007 return NewProcessCookieTest(ProcessCookieTestOpts{
1008 providerValidateCookieResponse: true,
1009 }, modifiers...)
1012func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState) error {
1013 err := p.proxy.SaveSession(p.rw, p.req, s)
1014 if err != nil {
1015 return err
1017 for _, cookie := range p.rw.Result().Cookies() {
1018 p.req.AddCookie(cookie)
1020 return nil
1023func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error) {
1024 return p.proxy.LoadCookiedSession(p.req)
1027func TestLoadCookiedSession(t *testing.T) {
1028 pcTest, err := NewProcessCookieTestWithDefaults()
1029 if err != nil {
1030 t.Fatal(err)
1033 created := time.Now()
1034 startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: &created}
1035 err = pcTest.SaveSession(startSession)
1036 assert.NoError(t, err)
1038 session, err := pcTest.LoadCookiedSession()
1039 if err != nil {
1040 t.Fatal(err)
1042 assert.Equal(t, startSession.Email, session.Email)
1043 assert.Equal(t, "", session.User)
1044 assert.Equal(t, startSession.AccessToken, session.AccessToken)
1047func TestProcessCookieNoCookieError(t *testing.T) {
1048 pcTest, err := NewProcessCookieTestWithDefaults()
1049 if err != nil {
1050 t.Fatal(err)
1053 session, err := pcTest.LoadCookiedSession()
1054 assert.Error(t, err, "cookie \"_oauth2_proxy\" not present")
1055 if session != nil {
1056 t.Errorf("expected nil session. got %#v", session)
1060func TestProcessCookieRefreshNotSet(t *testing.T) {
1061 pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1062 opts.Cookie.Expire = time.Duration(23) * time.Hour
1063 })
1064 if err != nil {
1065 t.Fatal(err)
1068 reference := time.Now().Add(time.Duration(-2) * time.Hour)
1070 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1071 err = pcTest.SaveSession(startSession)
1072 assert.NoError(t, err)
1074 session, err := pcTest.LoadCookiedSession()
1075 assert.Equal(t, nil, err)
1076 if session.Age() < time.Duration(-2)*time.Hour {
1077 t.Errorf("cookie too young %v", session.Age())
1079 assert.Equal(t, startSession.Email, session.Email)
1082func TestProcessCookieFailIfCookieExpired(t *testing.T) {
1083 pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1084 opts.Cookie.Expire = time.Duration(24) * time.Hour
1085 })
1086 if err != nil {
1087 t.Fatal(err)
1090 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
1091 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1092 err = pcTest.SaveSession(startSession)
1093 assert.NoError(t, err)
1095 session, err := pcTest.LoadCookiedSession()
1096 assert.NotEqual(t, nil, err)
1097 if session != nil {
1098 t.Errorf("expected nil session %#v", session)
1102func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
1103 pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1104 opts.Cookie.Expire = time.Duration(24) * time.Hour
1105 })
1106 if err != nil {
1107 t.Fatal(err)
1110 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
1111 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1112 err = pcTest.SaveSession(startSession)
1113 assert.NoError(t, err)
1115 pcTest.proxy.CookieRefresh = time.Hour
1116 session, err := pcTest.LoadCookiedSession()
1117 assert.NotEqual(t, nil, err)
1118 if session != nil {
1119 t.Errorf("expected nil session %#v", session)
1123func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
1124 pcTest, err := NewProcessCookieTestWithDefaults()
1125 if err != nil {
1126 return nil, err
1128 pcTest.req, _ = http.NewRequest("GET",
1129 pcTest.opts.ProxyPrefix+"/userinfo", nil)
1130 return pcTest, nil
1133func TestUserInfoEndpointAccepted(t *testing.T) {
1134 testCases := []struct {
1135 name string
1136 session *sessions.SessionState
1137 expectedResponse string
1138 }{
1140 name: "Full session",
1141 session: &sessions.SessionState{
1142 User: "john.doe",
1143 Email: "john.doe@example.com",
1144 Groups: []string{"example", "groups"},
1145 AccessToken: "my_access_token",
1146 },
1147 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"]}\n",
1148 },
1150 name: "Minimal session",
1151 session: &sessions.SessionState{
1152 User: "john.doe",
1153 Email: "john.doe@example.com",
1154 Groups: []string{"example", "groups"},
1155 },
1156 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"]}\n",
1157 },
1159 name: "No groups",
1160 session: &sessions.SessionState{
1161 User: "john.doe",
1162 Email: "john.doe@example.com",
1163 AccessToken: "my_access_token",
1164 },
1165 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\"}\n",
1166 },
1168 name: "With Preferred Username",
1169 session: &sessions.SessionState{
1170 User: "john.doe",
1171 PreferredUsername: "john",
1172 Email: "john.doe@example.com",
1173 Groups: []string{"example", "groups"},
1174 AccessToken: "my_access_token",
1175 },
1176 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\"}\n",
1177 },
1180 for _, tc := range testCases {
1181 t.Run(tc.name, func(t *testing.T) {
1182 test, err := NewUserInfoEndpointTest()
1183 if err != nil {
1184 t.Fatal(err)
1186 err = test.SaveSession(tc.session)
1187 assert.NoError(t, err)
1189 test.proxy.ServeHTTP(test.rw, test.req)
1190 assert.Equal(t, http.StatusOK, test.rw.Code)
1191 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1192 assert.Equal(t, tc.expectedResponse, string(bodyBytes))
1193 })
1197func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1198 test, err := NewUserInfoEndpointTest()
1199 if err != nil {
1200 t.Fatal(err)
1203 test.proxy.ServeHTTP(test.rw, test.req)
1204 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1207func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1208 pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
1209 if err != nil {
1210 return nil, err
1212 pcTest.req, _ = http.NewRequest(
1213 "GET",
1214 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1215 nil)
1216 return pcTest, nil
1219func TestAuthOnlyEndpointAccepted(t *testing.T) {
1220 test, err := NewAuthOnlyEndpointTest("")
1221 if err != nil {
1222 t.Fatal(err)
1225 created := time.Now()
1226 startSession := &sessions.SessionState{
1227 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created}
1228 err = test.SaveSession(startSession)
1229 assert.NoError(t, err)
1231 test.proxy.ServeHTTP(test.rw, test.req)
1232 assert.Equal(t, http.StatusAccepted, test.rw.Code)
1233 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1234 assert.Equal(t, "", string(bodyBytes))
1237func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1238 test, err := NewAuthOnlyEndpointTest("")
1239 if err != nil {
1240 t.Fatal(err)
1243 test.proxy.ServeHTTP(test.rw, test.req)
1244 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1245 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1246 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1249func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1250 test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1251 opts.Cookie.Expire = time.Duration(24) * time.Hour
1252 })
1253 if err != nil {
1254 t.Fatal(err)
1257 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
1258 startSession := &sessions.SessionState{
1259 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1260 err = test.SaveSession(startSession)
1261 assert.NoError(t, err)
1263 test.proxy.ServeHTTP(test.rw, test.req)
1264 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1265 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1266 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1269func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
1270 test, err := NewAuthOnlyEndpointTest("")
1271 if err != nil {
1272 t.Fatal(err)
1275 created := time.Now()
1276 startSession := &sessions.SessionState{
1277 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created}
1278 err = test.SaveSession(startSession)
1279 assert.NoError(t, err)
1280 test.validateUser = false
1282 test.proxy.ServeHTTP(test.rw, test.req)
1283 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1284 bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
1285 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1288func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
1289 var pcTest ProcessCookieTest
1291 pcTest.opts = baseTestOptions()
1292 pcTest.opts.InjectResponseHeaders = []options.Header{
1294 Name: "X-Auth-Request-User",
1295 Values: []options.HeaderValue{
1297 ClaimSource: &options.ClaimSource{
1298 Claim: "user",
1299 },
1300 },
1301 },
1302 },
1304 Name: "X-Auth-Request-Email",
1305 Values: []options.HeaderValue{
1307 ClaimSource: &options.ClaimSource{
1308 Claim: "email",
1309 },
1310 },
1311 },
1312 },
1314 Name: "X-Auth-Request-Groups",
1315 Values: []options.HeaderValue{
1317 ClaimSource: &options.ClaimSource{
1318 Claim: "groups",
1319 },
1320 },
1321 },
1322 },
1324 Name: "X-Forwarded-Preferred-Username",
1325 Values: []options.HeaderValue{
1327 ClaimSource: &options.ClaimSource{
1328 Claim: "preferred_username",
1329 },
1330 },
1331 },
1332 },
1334 pcTest.opts.AllowedGroups = []string{"oauth_groups"}
1335 err := validation.Validate(pcTest.opts)
1336 assert.NoError(t, err)
1338 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
1339 return pcTest.validateUser
1340 })
1341 if err != nil {
1342 t.Fatal(err)
1344 pcTest.proxy.provider = &TestProvider{
1345 ProviderData: &providers.ProviderData{},
1346 ValidToken: true,
1349 pcTest.validateUser = true
1351 pcTest.rw = httptest.NewRecorder()
1352 pcTest.req, _ = http.NewRequest("GET",
1353 pcTest.opts.ProxyPrefix+"/auth", nil)
1355 created := time.Now()
1356 startSession := &sessions.SessionState{
1357 User: "oauth_user", Groups: []string{"oauth_groups"}, Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1358 err = pcTest.SaveSession(startSession)
1359 assert.NoError(t, err)
1361 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
1362 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
1363 assert.Equal(t, "oauth_user", pcTest.rw.Header().Get("X-Auth-Request-User"))
1364 assert.Equal(t, startSession.Groups, pcTest.rw.Header().Values("X-Auth-Request-Groups"))
1365 assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Get("X-Auth-Request-Email"))
1368func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
1369 var pcTest ProcessCookieTest
1371 pcTest.opts = baseTestOptions()
1372 pcTest.opts.InjectResponseHeaders = []options.Header{
1374 Name: "X-Auth-Request-User",
1375 Values: []options.HeaderValue{
1377 ClaimSource: &options.ClaimSource{
1378 Claim: "user",
1379 },
1380 },
1381 },
1382 },
1384 Name: "X-Auth-Request-Email",
1385 Values: []options.HeaderValue{
1387 ClaimSource: &options.ClaimSource{
1388 Claim: "email",
1389 },
1390 },
1391 },
1392 },
1394 Name: "X-Auth-Request-Groups",
1395 Values: []options.HeaderValue{
1397 ClaimSource: &options.ClaimSource{
1398 Claim: "groups",
1399 },
1400 },
1401 },
1402 },
1404 Name: "X-Forwarded-Preferred-Username",
1405 Values: []options.HeaderValue{
1407 ClaimSource: &options.ClaimSource{
1408 Claim: "preferred_username",
1409 },
1410 },
1411 },
1412 },
1414 Name: "Authorization",
1415 Values: []options.HeaderValue{
1417 ClaimSource: &options.ClaimSource{
1418 Claim: "user",
1419 BasicAuthPassword: &options.SecretSource{
1420 Value: []byte("This is a secure password"),
1421 },
1422 },
1423 },
1424 },
1425 },
1428 err := validation.Validate(pcTest.opts)
1429 assert.NoError(t, err)
1431 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
1432 return pcTest.validateUser
1433 })
1434 if err != nil {
1435 t.Fatal(err)
1437 pcTest.proxy.provider = &TestProvider{
1438 ProviderData: &providers.ProviderData{},
1439 ValidToken: true,
1442 pcTest.validateUser = true
1444 pcTest.rw = httptest.NewRecorder()
1445 pcTest.req, _ = http.NewRequest("GET",
1446 pcTest.opts.ProxyPrefix+"/auth", nil)
1448 created := time.Now()
1449 startSession := &sessions.SessionState{
1450 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1451 err = pcTest.SaveSession(startSession)
1452 assert.NoError(t, err)
1454 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
1455 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
1456 assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0])
1457 assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0])
1458 expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("oauth_user:This is a secure password"))
1459 assert.Equal(t, expectedHeader, pcTest.rw.Header().Values("Authorization")[0])
1462func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
1463 var pcTest ProcessCookieTest
1465 pcTest.opts = baseTestOptions()
1466 pcTest.opts.InjectResponseHeaders = []options.Header{
1468 Name: "X-Auth-Request-User",
1469 Values: []options.HeaderValue{
1471 ClaimSource: &options.ClaimSource{
1472 Claim: "user",
1473 },
1474 },
1475 },
1476 },
1478 Name: "X-Auth-Request-Email",
1479 Values: []options.HeaderValue{
1481 ClaimSource: &options.ClaimSource{
1482 Claim: "email",
1483 },
1484 },
1485 },
1486 },
1488 Name: "X-Auth-Request-Groups",
1489 Values: []options.HeaderValue{
1491 ClaimSource: &options.ClaimSource{
1492 Claim: "groups",
1493 },
1494 },
1495 },
1496 },
1498 Name: "X-Forwarded-Preferred-Username",
1499 Values: []options.HeaderValue{
1501 ClaimSource: &options.ClaimSource{
1502 Claim: "preferred_username",
1503 },
1504 },
1505 },
1506 },
1508 err := validation.Validate(pcTest.opts)
1509 assert.NoError(t, err)
1511 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
1512 return pcTest.validateUser
1513 })
1514 if err != nil {
1515 t.Fatal(err)
1517 pcTest.proxy.provider = &TestProvider{
1518 ProviderData: &providers.ProviderData{},
1519 ValidToken: true,
1522 pcTest.validateUser = true
1524 pcTest.rw = httptest.NewRecorder()
1525 pcTest.req, _ = http.NewRequest("GET",
1526 pcTest.opts.ProxyPrefix+"/auth", nil)
1528 created := time.Now()
1529 startSession := &sessions.SessionState{
1530 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1531 err = pcTest.SaveSession(startSession)
1532 assert.NoError(t, err)
1534 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
1535 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
1536 assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0])
1537 assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0])
1538 assert.Equal(t, 0, len(pcTest.rw.Header().Values("Authorization")), "should not have Authorization header entries")
1541func TestAuthSkippedForPreflightRequests(t *testing.T) {
1542 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1543 w.WriteHeader(200)
1544 _, err := w.Write([]byte("response"))
1545 if err != nil {
1546 t.Fatal(err)
1548 }))
1549 t.Cleanup(upstreamServer.Close)
1551 opts := baseTestOptions()
1552 opts.UpstreamServers = options.Upstreams{
1554 ID: upstreamServer.URL,
1555 Path: "/",
1556 URI: upstreamServer.URL,
1557 },
1559 opts.SkipAuthPreflight = true
1560 err := validation.Validate(opts)
1561 assert.NoError(t, err)
1563 upstreamURL, _ := url.Parse(upstreamServer.URL)
1564 opts.SetProvider(NewTestProvider(upstreamURL, ""))
1566 proxy, err := NewOAuthProxy(opts, func(string) bool { return false })
1567 if err != nil {
1568 t.Fatal(err)
1570 rw := httptest.NewRecorder()
1571 req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil)
1572 proxy.ServeHTTP(rw, req)
1574 assert.Equal(t, 200, rw.Code)
1575 assert.Equal(t, "response", rw.Body.String())
1578type SignatureAuthenticator struct {
1579 auth hmacauth.HmacAuth
1582func (v *SignatureAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) {
1583 result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
1585 var msg string
1586 switch result {
1587 case hmacauth.ResultNoSignature:
1588 msg = "no signature received"
1589 case hmacauth.ResultMatch:
1590 msg = "signatures match"
1591 case hmacauth.ResultMismatch:
1592 msg = fmt.Sprintf(
1593 "signatures do not match:\n received: %s\n computed: %s",
1594 headerSig,
1595 computedSig)
1596 default:
1597 panic("unknown result value: " + result.String())
1600 _, err := w.Write([]byte(msg))
1601 if err != nil {
1602 panic(err)
1606type SignatureTest struct {
1607 opts *options.Options
1608 upstream *httptest.Server
1609 upstreamHost string
1610 provider *httptest.Server
1611 header http.Header
1612 rw *httptest.ResponseRecorder
1613 authenticator *SignatureAuthenticator
1616func NewSignatureTest() (*SignatureTest, error) {
1617 opts := baseTestOptions()
1618 opts.EmailDomains = []string{"acm.org"}
1620 authenticator := &SignatureAuthenticator{}
1621 upstreamServer := httptest.NewServer(
1622 http.HandlerFunc(authenticator.Authenticate))
1623 upstreamURL, err := url.Parse(upstreamServer.URL)
1624 if err != nil {
1625 return nil, err
1627 opts.UpstreamServers = options.Upstreams{
1629 ID: upstreamServer.URL,
1630 Path: "/",
1631 URI: upstreamServer.URL,
1632 },
1635 providerHandler := func(w http.ResponseWriter, r *http.Request) {
1636 _, err := w.Write([]byte(`{"access_token": "my_auth_token"}`))
1637 if err != nil {
1638 panic(err)
1641 provider := httptest.NewServer(http.HandlerFunc(providerHandler))
1642 providerURL, err := url.Parse(provider.URL)
1643 if err != nil {
1644 return nil, err
1646 opts.SetProvider(NewTestProvider(providerURL, "mbland@acm.org"))
1648 return &SignatureTest{
1649 opts,
1650 upstreamServer,
1651 upstreamURL.Host,
1652 provider,
1653 make(http.Header),
1654 httptest.NewRecorder(),
1655 authenticator,
1656 }, nil
1659func (st *SignatureTest) Close() {
1660 st.provider.Close()
1661 st.upstream.Close()
1664// fakeNetConn simulates an http.Request.Body buffer that will be consumed
1665// when it is read by the hmacauth.HmacAuth if not handled properly. See:
1666// https://github.com/18F/hmacauth/pull/4
1667type fakeNetConn struct {
1668 reqBody string
1671func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
1672 if bodyLen := len(fnc.reqBody); bodyLen != 0 {
1673 copy(p, fnc.reqBody)
1674 fnc.reqBody = ""
1675 return bodyLen, io.EOF
1677 return 0, io.EOF
1680func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) error {
1681 err := validation.Validate(st.opts)
1682 if err != nil {
1683 return err
1685 proxy, err := NewOAuthProxy(st.opts, func(email string) bool { return true })
1686 if err != nil {
1687 return err
1690 var bodyBuf io.ReadCloser
1691 if body != "" {
1692 bodyBuf = ioutil.NopCloser(&fakeNetConn{reqBody: body})
1694 req := httptest.NewRequest(method, "/foo/bar", bodyBuf)
1695 req.Header = st.header
1697 state := &sessions.SessionState{
1698 Email: "mbland@acm.org", AccessToken: "my_access_token"}
1699 err = proxy.SaveSession(st.rw, req, state)
1700 if err != nil {
1701 return err
1703 for _, c := range st.rw.Result().Cookies() {
1704 req.AddCookie(c)
1706 // This is used by the upstream to validate the signature.
1707 st.authenticator.auth = hmacauth.NewHmacAuth(
1708 crypto.SHA1, []byte(key), upstream.SignatureHeader, upstream.SignatureHeaders)
1709 proxy.ServeHTTP(st.rw, req)
1711 return nil
1714func TestRequestSignature(t *testing.T) {
1715 testCases := map[string]struct {
1716 method string
1717 body string
1718 key string
1719 resp string
1720 }{
1721 "No request signature": {
1722 method: "GET",
1723 body: "",
1724 key: "",
1725 resp: "no signature received",
1726 },
1727 "Get request": {
1728 method: "GET",
1729 body: "",
1730 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1731 resp: "signatures match",
1732 },
1733 "Post request": {
1734 method: "POST",
1735 body: `{ "hello": "world!" }`,
1736 key: "d90df39e2d19282840252612dd7c81421a372f61",
1737 resp: "signatures match",
1738 },
1740 for name, tc := range testCases {
1741 t.Run(name, func(t *testing.T) {
1742 st, err := NewSignatureTest()
1743 if err != nil {
1744 t.Fatal(err)
1746 t.Cleanup(st.Close)
1747 if tc.key != "" {
1748 st.opts.SignatureKey = fmt.Sprintf("sha1:%s", tc.key)
1750 err = st.MakeRequestWithExpectedKey(tc.method, tc.body, tc.key)
1751 assert.NoError(t, err)
1752 assert.Equal(t, 200, st.rw.Code)
1753 assert.Equal(t, tc.resp, st.rw.Body.String())
1754 })
1758func Test_getAppRedirect(t *testing.T) {
1759 opts := baseTestOptions()
1760 opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com", ".example.com:8443")
1761 err := validation.Validate(opts)
1762 assert.NoError(t, err)
1763 require.NotEmpty(t, opts.ProxyPrefix)
1764 proxy, err := NewOAuthProxy(opts, func(s string) bool { return false })
1765 if err != nil {
1766 t.Fatal(err)
1769 tests := []struct {
1770 name string
1771 url string
1772 headers map[string]string
1773 reverseProxy bool
1774 expectedRedirect string
1775 }{
1777 name: "request outside of ProxyPrefix redirects to original URL",
1778 url: "/foo/bar",
1779 headers: nil,
1780 reverseProxy: false,
1781 expectedRedirect: "/foo/bar",
1782 },
1784 name: "request with query preserves query",
1785 url: "/foo?bar",
1786 headers: nil,
1787 reverseProxy: false,
1788 expectedRedirect: "/foo?bar",
1789 },
1791 name: "request under ProxyPrefix redirects to root",
1792 url: proxy.ProxyPrefix + "/foo/bar",
1793 headers: nil,
1794 reverseProxy: false,
1795 expectedRedirect: "/",
1796 },
1798 name: "proxied request outside of ProxyPrefix redirects to proxied URL",
1799 url: "https://oauth.example.com/foo/bar",
1800 headers: map[string]string{
1801 "X-Forwarded-Proto": "https",
1802 "X-Forwarded-Host": "a-service.example.com",
1803 "X-Forwarded-Uri": "/foo/bar",
1804 },
1805 reverseProxy: true,
1806 expectedRedirect: "https://a-service.example.com/foo/bar",
1807 },
1809 name: "non-proxied request with spoofed proxy headers wouldn't redirect",
1810 url: "https://oauth.example.com/foo?bar",
1811 headers: map[string]string{
1812 "X-Forwarded-Proto": "https",
1813 "X-Forwarded-Host": "a-service.example.com",
1814 "X-Forwarded-Uri": "/foo/bar",
1815 },
1816 reverseProxy: false,
1817 expectedRedirect: "/foo?bar",
1818 },
1820 name: "proxied request under ProxyPrefix redirects to root",
1821 url: "https://oauth.example.com" + proxy.ProxyPrefix + "/foo/bar",
1822 headers: map[string]string{
1823 "X-Forwarded-Proto": "https",
1824 "X-Forwarded-Host": "a-service.example.com",
1825 "X-Forwarded-Uri": proxy.ProxyPrefix + "/foo/bar",
1826 },
1827 reverseProxy: true,
1828 expectedRedirect: "https://a-service.example.com/",
1829 },
1831 name: "proxied request with port under ProxyPrefix redirects to root",
1832 url: "https://oauth.example.com" + proxy.ProxyPrefix + "/foo/bar",
1833 headers: map[string]string{
1834 "X-Forwarded-Proto": "https",
1835 "X-Forwarded-Host": "a-service.example.com:8443",
1836 "X-Forwarded-Uri": proxy.ProxyPrefix + "/foo/bar",
1837 },
1838 reverseProxy: true,
1839 expectedRedirect: "https://a-service.example.com:8443/",
1840 },
1842 name: "proxied request with missing uri header would still redirect to desired redirect",
1843 url: "https://oauth.example.com/foo?bar",
1844 headers: map[string]string{
1845 "X-Forwarded-Proto": "https",
1846 "X-Forwarded-Host": "a-service.example.com",
1847 },
1848 reverseProxy: true,
1849 expectedRedirect: "https://a-service.example.com/foo?bar",
1850 },
1852 name: "request with headers proxy not being set (and reverse proxy enabled) would still redirect to desired redirect",
1853 url: "https://oauth.example.com/foo?bar",
1854 headers: nil,
1855 reverseProxy: true,
1856 expectedRedirect: "/foo?bar",
1857 },
1859 name: "proxied request with X-Auth-Request-Redirect being set outside of ProxyPrefix redirects to proxied URL",
1860 url: "https://oauth.example.com/foo/bar",
1861 headers: map[string]string{
1862 "X-Auth-Request-Redirect": "https://a-service.example.com/foo/bar",
1863 },
1864 reverseProxy: true,
1865 expectedRedirect: "https://a-service.example.com/foo/bar",
1866 },
1868 name: "proxied request with rd query string redirects to proxied URL",
1869 url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbar",
1870 headers: nil,
1871 reverseProxy: false,
1872 expectedRedirect: "https://a-service.example.com/foo/bar",
1873 },
1875 name: "proxied request with rd query string and all headers set (and reverse proxy not enabled) redirects to proxied URL on rd query string",
1876 url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
1877 headers: map[string]string{
1878 "X-Auth-Request-Redirect": "https://a-service.example.com/foo/baz",
1879 "X-Forwarded-Proto": "http",
1880 "X-Forwarded-Host": "another-service.example.com",
1881 "X-Forwarded-Uri": "/seasons/greetings",
1882 },
1883 reverseProxy: false,
1884 expectedRedirect: "https://a-service.example.com/foo/jazz",
1885 },
1887 name: "proxied request with rd query string and some headers set redirects to proxied URL on rd query string",
1888 url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbaz",
1889 headers: map[string]string{
1890 "X-Forwarded-Proto": "https",
1891 "X-Forwarded-Host": "another-service.example.com",
1892 "X-Forwarded-Uri": "/seasons/greetings",
1893 },
1894 reverseProxy: true,
1895 expectedRedirect: "https://a-service.example.com/foo/baz",
1896 },
1898 for _, tt := range tests {
1899 t.Run(tt.name, func(t *testing.T) {
1900 req, _ := http.NewRequest("GET", tt.url, nil)
1901 for header, value := range tt.headers {
1902 if value != "" {
1903 req.Header.Add(header, value)
1906 req = middleware.AddRequestScope(req, &middleware.RequestScope{
1907 ReverseProxy: tt.reverseProxy,
1908 })
1909 redirect, err := proxy.getAppRedirect(req)
1911 assert.NoError(t, err)
1912 assert.Equal(t, tt.expectedRedirect, redirect)
1913 })
1917type ajaxRequestTest struct {
1918 opts *options.Options
1919 proxy *OAuthProxy
1922func newAjaxRequestTest() (*ajaxRequestTest, error) {
1923 test := &ajaxRequestTest{}
1924 test.opts = baseTestOptions()
1925 err := validation.Validate(test.opts)
1926 if err != nil {
1927 return nil, err
1930 test.proxy, err = NewOAuthProxy(test.opts, func(email string) bool {
1931 return true
1932 })
1933 if err != nil {
1934 return nil, err
1936 return test, nil
1939func (test *ajaxRequestTest) getEndpoint(endpoint string, header http.Header) (int, http.Header, error) {
1940 rw := httptest.NewRecorder()
1941 req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
1942 if err != nil {
1943 return 0, nil, err
1945 req.Header = header
1946 test.proxy.ServeHTTP(rw, req)
1947 return rw.Code, rw.Header(), nil
1950func testAjaxUnauthorizedRequest(t *testing.T, header http.Header) {
1951 test, err := newAjaxRequestTest()
1952 if err != nil {
1953 t.Fatal(err)
1955 endpoint := "/test"
1957 code, rh, err := test.getEndpoint(endpoint, header)
1958 assert.NoError(t, err)
1959 assert.Equal(t, http.StatusUnauthorized, code)
1960 mime := rh.Get("Content-Type")
1961 assert.Equal(t, applicationJSON, mime)
1963func TestAjaxUnauthorizedRequest1(t *testing.T) {
1964 header := make(http.Header)
1965 header.Add("accept", applicationJSON)
1967 testAjaxUnauthorizedRequest(t, header)
1970func TestAjaxUnauthorizedRequest2(t *testing.T) {
1971 header := make(http.Header)
1972 header.Add("Accept", applicationJSON)
1974 testAjaxUnauthorizedRequest(t, header)
1977func TestAjaxUnauthorizedRequestAccept1(t *testing.T) {
1978 header := make(http.Header)
1979 header.Add("Accept", "application/json, text/plain, */*")
1981 testAjaxUnauthorizedRequest(t, header)
1984func TestAjaxForbiddendRequest(t *testing.T) {
1985 test, err := newAjaxRequestTest()
1986 if err != nil {
1987 t.Fatal(err)
1989 endpoint := "/test"
1990 header := make(http.Header)
1991 code, rh, err := test.getEndpoint(endpoint, header)
1992 assert.NoError(t, err)
1993 assert.Equal(t, http.StatusForbidden, code)
1994 mime := rh.Get("Content-Type")
1995 assert.NotEqual(t, applicationJSON, mime)
1998func TestClearSplitCookie(t *testing.T) {
1999 opts := baseTestOptions()
2000 opts.Cookie.Secret = base64CookieSecret
2001 opts.Cookie.Name = "oauth2"
2002 opts.Cookie.Domains = []string{"abc"}
2003 err := validation.Validate(opts)
2004 assert.NoError(t, err)
2006 store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
2007 if err != nil {
2008 t.Fatal(err)
2011 p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store}
2012 var rw = httptest.NewRecorder()
2013 req := httptest.NewRequest("get", "/", nil)
2015 req.AddCookie(&http.Cookie{
2016 Name: "test1",
2017 Value: "test1",
2018 })
2019 req.AddCookie(&http.Cookie{
2020 Name: "oauth2_0",
2021 Value: "oauth2_0",
2022 })
2023 req.AddCookie(&http.Cookie{
2024 Name: "oauth2_1",
2025 Value: "oauth2_1",
2026 })
2028 err = p.ClearSessionCookie(rw, req)
2029 assert.NoError(t, err)
2030 header := rw.Header()
2032 assert.Equal(t, 2, len(header["Set-Cookie"]), "should have 3 set-cookie header entries")
2035func TestClearSingleCookie(t *testing.T) {
2036 opts := baseTestOptions()
2037 opts.Cookie.Name = "oauth2"
2038 opts.Cookie.Domains = []string{"abc"}
2039 store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
2040 if err != nil {
2041 t.Fatal(err)
2044 p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store}
2045 var rw = httptest.NewRecorder()
2046 req := httptest.NewRequest("get", "/", nil)
2048 req.AddCookie(&http.Cookie{
2049 Name: "test1",
2050 Value: "test1",
2051 })
2052 req.AddCookie(&http.Cookie{
2053 Name: "oauth2",
2054 Value: "oauth2",
2055 })
2057 err = p.ClearSessionCookie(rw, req)
2058 assert.NoError(t, err)
2059 header := rw.Header()
2061 assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
2064type NoOpKeySet struct {
2067func (NoOpKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
2068 splitStrings := strings.Split(jwt, ".")
2069 payloadString := splitStrings[1]
2070 return base64.RawURLEncoding.DecodeString(payloadString)
2073func TestGetJwtSession(t *testing.T) {
2074 /* token payload:
2076 "sub": "1234567890",
2077 "aud": "https://test.myapp.com",
2078 "name": "John Doe",
2079 "email": "john@example.com",
2080 "iss": "https://issuer.example.com",
2081 "iat": 1553691215,
2082 "exp": 1912151821
2084 */
2085 goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
2086 "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
2087 "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
2088 "E1LCJleHAiOjE5MTIxNTE4MjF9." +
2089 "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
2090 "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
2092 keyset := NoOpKeySet{}
2093 verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
2094 &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
2096 test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
2097 opts.InjectRequestHeaders = []options.Header{
2099 Name: "Authorization",
2100 Values: []options.HeaderValue{
2102 ClaimSource: &options.ClaimSource{
2103 Claim: "id_token",
2104 Prefix: "Bearer ",
2105 },
2106 },
2107 },
2108 },
2110 Name: "X-Forwarded-User",
2111 Values: []options.HeaderValue{
2113 ClaimSource: &options.ClaimSource{
2114 Claim: "user",
2115 },
2116 },
2117 },
2118 },
2120 Name: "X-Forwarded-Email",
2121 Values: []options.HeaderValue{
2123 ClaimSource: &options.ClaimSource{
2124 Claim: "email",
2125 },
2126 },
2127 },
2128 },
2131 opts.InjectResponseHeaders = []options.Header{
2133 Name: "Authorization",
2134 Values: []options.HeaderValue{
2136 ClaimSource: &options.ClaimSource{
2137 Claim: "id_token",
2138 Prefix: "Bearer ",
2139 },
2140 },
2141 },
2142 },
2144 Name: "X-Auth-Request-User",
2145 Values: []options.HeaderValue{
2147 ClaimSource: &options.ClaimSource{
2148 Claim: "user",
2149 },
2150 },
2151 },
2152 },
2154 Name: "X-Auth-Request-Email",
2155 Values: []options.HeaderValue{
2157 ClaimSource: &options.ClaimSource{
2158 Claim: "email",
2159 },
2160 },
2161 },
2162 },
2164 opts.SkipJwtBearerTokens = true
2165 opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier))
2166 })
2167 if err != nil {
2168 t.Fatal(err)
2170 tp, _ := test.proxy.provider.(*TestProvider)
2171 tp.GroupValidator = func(s string) bool {
2172 return true
2175 authHeader := fmt.Sprintf("Bearer %s", goodJwt)
2176 test.req.Header = map[string][]string{
2177 "Authorization": {authHeader},
2180 test.proxy.ServeHTTP(test.rw, test.req)
2181 if test.rw.Code >= 400 {
2182 t.Fatalf("expected 3xx got %d", test.rw.Code)
2185 // Check PassAuthorization, should overwrite Basic header
2186 assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
2187 assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "1234567890")
2188 assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
2190 // SetAuthorization and SetXAuthRequest
2191 assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
2192 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "1234567890")
2193 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
2196func Test_prepareNoCache(t *testing.T) {
2197 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2198 prepareNoCache(w)
2199 })
2200 mux := http.NewServeMux()
2201 mux.Handle("/", handler)
2203 rec := httptest.NewRecorder()
2204 req := httptest.NewRequest(http.MethodGet, "/", nil)
2205 mux.ServeHTTP(rec, req)
2207 for k, v := range noCacheHeaders {
2208 assert.Equal(t, rec.Header().Get(k), v)
2212func Test_noCacheHeaders(t *testing.T) {
2213 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2214 _, err := w.Write([]byte("upstream"))
2215 if err != nil {
2216 t.Error(err)
2218 }))
2219 t.Cleanup(upstreamServer.Close)
2221 opts := baseTestOptions()
2222 opts.UpstreamServers = options.Upstreams{
2224 ID: upstreamServer.URL,
2225 Path: "/",
2226 URI: upstreamServer.URL,
2227 },
2229 opts.SkipAuthRegex = []string{".*"}
2230 err := validation.Validate(opts)
2231 assert.NoError(t, err)
2232 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2233 if err != nil {
2234 t.Fatal(err)
2237 t.Run("not exist in response from upstream", func(t *testing.T) {
2238 rec := httptest.NewRecorder()
2239 req := httptest.NewRequest(http.MethodGet, "/upstream", nil)
2240 proxy.ServeHTTP(rec, req)
2242 assert.Equal(t, http.StatusOK, rec.Code)
2243 assert.Equal(t, "upstream", rec.Body.String())
2245 // checking noCacheHeaders does not exists in response headers from upstream
2246 for k := range noCacheHeaders {
2247 assert.Equal(t, "", rec.Header().Get(k))
2249 })
2251 t.Run("has no-cache", func(t *testing.T) {
2252 tests := []struct {
2253 path string
2254 hasNoCache bool
2255 }{
2257 path: "/oauth2/sign_in",
2258 hasNoCache: true,
2259 },
2261 path: "/oauth2/sign_out",
2262 hasNoCache: true,
2263 },
2265 path: "/oauth2/start",
2266 hasNoCache: true,
2267 },
2269 path: "/oauth2/callback",
2270 hasNoCache: true,
2271 },
2273 path: "/oauth2/auth",
2274 hasNoCache: false,
2275 },
2277 path: "/oauth2/userinfo",
2278 hasNoCache: true,
2279 },
2281 path: "/upstream",
2282 hasNoCache: false,
2283 },
2286 for _, tt := range tests {
2287 t.Run(tt.path, func(t *testing.T) {
2288 rec := httptest.NewRecorder()
2289 req := httptest.NewRequest(http.MethodGet, tt.path, nil)
2290 proxy.ServeHTTP(rec, req)
2291 cacheControl := rec.Result().Header.Get("Cache-Control")
2292 if tt.hasNoCache != (strings.Contains(cacheControl, "no-cache")) {
2293 t.Errorf(`unexpected "Cache-Control" header: %s`, cacheControl)
2295 })
2298 })
2301func baseTestOptions() *options.Options {
2302 opts := options.NewOptions()
2303 opts.Cookie.Secret = rawCookieSecret
2304 opts.ClientID = clientID
2305 opts.ClientSecret = clientSecret
2306 opts.EmailDomains = []string{"*"}
2308 // Default injected headers for legacy configuration
2309 opts.InjectRequestHeaders = []options.Header{
2311 Name: "Authorization",
2312 Values: []options.HeaderValue{
2314 ClaimSource: &options.ClaimSource{
2315 Claim: "user",
2316 BasicAuthPassword: &options.SecretSource{
2317 Value: []byte(base64.StdEncoding.EncodeToString([]byte("This is a secure password"))),
2318 },
2319 },
2320 },
2321 },
2322 },
2324 Name: "X-Forwarded-User",
2325 Values: []options.HeaderValue{
2327 ClaimSource: &options.ClaimSource{
2328 Claim: "user",
2329 },
2330 },
2331 },
2332 },
2334 Name: "X-Forwarded-Email",
2335 Values: []options.HeaderValue{
2337 ClaimSource: &options.ClaimSource{
2338 Claim: "email",
2339 },
2340 },
2341 },
2342 },
2345 return opts
2348func TestTrustedIPs(t *testing.T) {
2349 tests := []struct {
2350 name string
2351 trustedIPs []string
2352 reverseProxy bool
2353 realClientIPHeader string
2354 req *http.Request
2355 expectTrusted bool
2356 }{
2357 // Check unconfigured behavior.
2359 name: "Default",
2360 trustedIPs: nil,
2361 reverseProxy: false,
2362 realClientIPHeader: "X-Real-IP", // Default value
2363 req: func() *http.Request {
2364 req, _ := http.NewRequest("GET", "/", nil)
2365 return req
2366 }(),
2367 expectTrusted: false,
2368 },
2369 // Check using req.RemoteAddr (Options.ReverseProxy == false).
2371 name: "WithRemoteAddr",
2372 trustedIPs: []string{"127.0.0.1"},
2373 reverseProxy: false,
2374 realClientIPHeader: "X-Real-IP", // Default value
2375 req: func() *http.Request {
2376 req, _ := http.NewRequest("GET", "/", nil)
2377 req.RemoteAddr = "127.0.0.1:43670"
2378 return req
2379 }(),
2380 expectTrusted: true,
2381 },
2382 // Check ignores req.RemoteAddr match when behind a reverse proxy / missing header.
2384 name: "IgnoresRemoteAddrInReverseProxyMode",
2385 trustedIPs: []string{"127.0.0.1"},
2386 reverseProxy: true,
2387 realClientIPHeader: "X-Real-IP", // Default value
2388 req: func() *http.Request {
2389 req, _ := http.NewRequest("GET", "/", nil)
2390 req.RemoteAddr = "127.0.0.1:44324"
2391 return req
2392 }(),
2393 expectTrusted: false,
2394 },
2395 // Check successful trusting of localhost in IPv4.
2397 name: "TrustsLocalhostInReverseProxyMode",
2398 trustedIPs: []string{"127.0.0.0/8", "::1"},
2399 reverseProxy: true,
2400 realClientIPHeader: "X-Forwarded-For",
2401 req: func() *http.Request {
2402 req, _ := http.NewRequest("GET", "/", nil)
2403 req.Header.Add("X-Forwarded-For", "127.0.0.1")
2404 return req
2405 }(),
2406 expectTrusted: true,
2407 },
2408 // Check successful trusting of localhost in IPv6.
2410 name: "TrustsIP6LocalostInReverseProxyMode",
2411 trustedIPs: []string{"127.0.0.0/8", "::1"},
2412 reverseProxy: true,
2413 realClientIPHeader: "X-Forwarded-For",
2414 req: func() *http.Request {
2415 req, _ := http.NewRequest("GET", "/", nil)
2416 req.Header.Add("X-Forwarded-For", "::1")
2417 return req
2418 }(),
2419 expectTrusted: true,
2420 },
2421 // Check does not trust random IPv4 address.
2423 name: "DoesNotTrustRandomIP4Address",
2424 trustedIPs: []string{"127.0.0.0/8", "::1"},
2425 reverseProxy: true,
2426 realClientIPHeader: "X-Forwarded-For",
2427 req: func() *http.Request {
2428 req, _ := http.NewRequest("GET", "/", nil)
2429 req.Header.Add("X-Forwarded-For", "12.34.56.78")
2430 return req
2431 }(),
2432 expectTrusted: false,
2433 },
2434 // Check does not trust random IPv6 address.
2436 name: "DoesNotTrustRandomIP6Address",
2437 trustedIPs: []string{"127.0.0.0/8", "::1"},
2438 reverseProxy: true,
2439 realClientIPHeader: "X-Forwarded-For",
2440 req: func() *http.Request {
2441 req, _ := http.NewRequest("GET", "/", nil)
2442 req.Header.Add("X-Forwarded-For", "::2")
2443 return req
2444 }(),
2445 expectTrusted: false,
2446 },
2447 // Check respects correct header.
2449 name: "RespectsCorrectHeaderInReverseProxyMode",
2450 trustedIPs: []string{"127.0.0.0/8", "::1"},
2451 reverseProxy: true,
2452 realClientIPHeader: "X-Forwarded-For",
2453 req: func() *http.Request {
2454 req, _ := http.NewRequest("GET", "/", nil)
2455 req.Header.Add("X-Real-IP", "::1")
2456 return req
2457 }(),
2458 expectTrusted: false,
2459 },
2460 // Check doesn't trust if garbage is provided.
2462 name: "DoesNotTrustGarbageInReverseProxyMode",
2463 trustedIPs: []string{"127.0.0.0/8", "::1"},
2464 reverseProxy: true,
2465 realClientIPHeader: "X-Forwarded-For",
2466 req: func() *http.Request {
2467 req, _ := http.NewRequest("GET", "/", nil)
2468 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2469 return req
2470 }(),
2471 expectTrusted: false,
2472 },
2473 // Check doesn't trust if garbage is provided (no reverse-proxy).
2475 name: "DoesNotTrustGarbage",
2476 trustedIPs: []string{"127.0.0.0/8", "::1"},
2477 reverseProxy: false,
2478 realClientIPHeader: "X-Real-IP",
2479 req: func() *http.Request {
2480 req, _ := http.NewRequest("GET", "/", nil)
2481 req.RemoteAddr = "adsfljk29242as!!"
2482 return req
2483 }(),
2484 expectTrusted: false,
2485 },
2488 for _, tt := range tests {
2489 t.Run(tt.name, func(t *testing.T) {
2490 opts := baseTestOptions()
2491 opts.UpstreamServers = options.Upstreams{
2493 ID: "static",
2494 Path: "/",
2495 Static: true,
2496 },
2498 opts.TrustedIPs = tt.trustedIPs
2499 opts.ReverseProxy = tt.reverseProxy
2500 opts.RealClientIPHeader = tt.realClientIPHeader
2501 err := validation.Validate(opts)
2502 assert.NoError(t, err)
2504 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
2505 assert.NoError(t, err)
2506 rw := httptest.NewRecorder()
2508 proxy.ServeHTTP(rw, tt.req)
2509 if tt.expectTrusted {
2510 assert.Equal(t, 200, rw.Code)
2511 } else {
2512 assert.Equal(t, 403, rw.Code)
2514 })
2518func Test_buildRoutesAllowlist(t *testing.T) {
2519 type expectedAllowedRoute struct {
2520 method string
2521 regexString string
2524 testCases := []struct {
2525 name string
2526 skipAuthRegex []string
2527 skipAuthRoutes []string
2528 expectedRoutes []expectedAllowedRoute
2529 shouldError bool
2530 }{
2532 name: "No skip auth configured",
2533 skipAuthRegex: []string{},
2534 skipAuthRoutes: []string{},
2535 expectedRoutes: []expectedAllowedRoute{},
2536 shouldError: false,
2537 },
2539 name: "Only skipAuthRegex configured",
2540 skipAuthRegex: []string{
2541 "^/foo/bar",
2542 "^/baz/[0-9]+/thing",
2543 },
2544 skipAuthRoutes: []string{},
2545 expectedRoutes: []expectedAllowedRoute{
2547 method: "",
2548 regexString: "^/foo/bar",
2549 },
2551 method: "",
2552 regexString: "^/baz/[0-9]+/thing",
2553 },
2554 },
2555 shouldError: false,
2556 },
2558 name: "Only skipAuthRoutes configured",
2559 skipAuthRegex: []string{},
2560 skipAuthRoutes: []string{
2561 "GET=^/foo/bar",
2562 "POST=^/baz/[0-9]+/thing",
2563 "^/all/methods$",
2564 "WEIRD=^/methods/are/allowed",
2565 "PATCH=/second/equals?are=handled&just=fine",
2566 },
2567 expectedRoutes: []expectedAllowedRoute{
2569 method: "GET",
2570 regexString: "^/foo/bar",
2571 },
2573 method: "POST",
2574 regexString: "^/baz/[0-9]+/thing",
2575 },
2577 method: "",
2578 regexString: "^/all/methods$",
2579 },
2581 method: "WEIRD",
2582 regexString: "^/methods/are/allowed",
2583 },
2585 method: "PATCH",
2586 regexString: "/second/equals?are=handled&just=fine",
2587 },
2588 },
2589 shouldError: false,
2590 },
2592 name: "Both skipAuthRegexes and skipAuthRoutes configured",
2593 skipAuthRegex: []string{
2594 "^/foo/bar/regex",
2595 "^/baz/[0-9]+/thing/regex",
2596 },
2597 skipAuthRoutes: []string{
2598 "GET=^/foo/bar",
2599 "POST=^/baz/[0-9]+/thing",
2600 "^/all/methods$",
2601 },
2602 expectedRoutes: []expectedAllowedRoute{
2604 method: "",
2605 regexString: "^/foo/bar/regex",
2606 },
2608 method: "",
2609 regexString: "^/baz/[0-9]+/thing/regex",
2610 },
2612 method: "GET",
2613 regexString: "^/foo/bar",
2614 },
2616 method: "POST",
2617 regexString: "^/baz/[0-9]+/thing",
2618 },
2620 method: "",
2621 regexString: "^/all/methods$",
2622 },
2623 },
2624 shouldError: false,
2625 },
2627 name: "Invalid skipAuthRegex entry",
2628 skipAuthRegex: []string{
2629 "^/foo/bar",
2630 "^/baz/[0-9]+/thing",
2631 "(bad[regex",
2632 },
2633 skipAuthRoutes: []string{},
2634 expectedRoutes: []expectedAllowedRoute{},
2635 shouldError: true,
2636 },
2638 name: "Invalid skipAuthRoutes entry",
2639 skipAuthRegex: []string{},
2640 skipAuthRoutes: []string{
2641 "GET=^/foo/bar",
2642 "POST=^/baz/[0-9]+/thing",
2643 "^/all/methods$",
2644 "PUT=(bad[regex",
2645 },
2646 expectedRoutes: []expectedAllowedRoute{},
2647 shouldError: true,
2648 },
2651 for _, tc := range testCases {
2652 t.Run(tc.name, func(t *testing.T) {
2653 opts := &options.Options{
2654 SkipAuthRegex: tc.skipAuthRegex,
2655 SkipAuthRoutes: tc.skipAuthRoutes,
2657 routes, err := buildRoutesAllowlist(opts)
2658 if tc.shouldError {
2659 assert.Error(t, err)
2660 return
2662 assert.NoError(t, err)
2664 for i, route := range routes {
2665 assert.Greater(t, len(tc.expectedRoutes), i)
2666 assert.Equal(t, route.method, tc.expectedRoutes[i].method)
2667 assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString)
2669 })
2673func TestAllowedRequest(t *testing.T) {
2674 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2675 w.WriteHeader(200)
2676 _, err := w.Write([]byte("Allowed Request"))
2677 if err != nil {
2678 t.Fatal(err)
2680 }))
2681 t.Cleanup(upstreamServer.Close)
2683 opts := baseTestOptions()
2684 opts.UpstreamServers = options.Upstreams{
2686 ID: upstreamServer.URL,
2687 Path: "/",
2688 URI: upstreamServer.URL,
2689 },
2691 opts.SkipAuthRegex = []string{
2692 "^/skip/auth/regex$",
2694 opts.SkipAuthRoutes = []string{
2695 "GET=^/skip/auth/routes/get",
2697 err := validation.Validate(opts)
2698 assert.NoError(t, err)
2699 proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2700 if err != nil {
2701 t.Fatal(err)
2704 testCases := []struct {
2705 name string
2706 method string
2707 url string
2708 allowed bool
2709 }{
2711 name: "Regex GET allowed",
2712 method: "GET",
2713 url: "/skip/auth/regex",
2714 allowed: true,
2715 },
2717 name: "Regex POST allowed ",
2718 method: "POST",
2719 url: "/skip/auth/regex",
2720 allowed: true,
2721 },
2723 name: "Regex denied",
2724 method: "GET",
2725 url: "/wrong/denied",
2726 allowed: false,
2727 },
2729 name: "Route allowed",
2730 method: "GET",
2731 url: "/skip/auth/routes/get",
2732 allowed: true,
2733 },
2735 name: "Route denied with wrong method",
2736 method: "PATCH",
2737 url: "/skip/auth/routes/get",
2738 allowed: false,
2739 },
2741 name: "Route denied with wrong path",
2742 method: "GET",
2743 url: "/skip/auth/routes/wrong/path",
2744 allowed: false,
2745 },
2748 for _, tc := range testCases {
2749 t.Run(tc.name, func(t *testing.T) {
2750 req, err := http.NewRequest(tc.method, tc.url, nil)
2751 assert.NoError(t, err)
2752 assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req))
2754 rw := httptest.NewRecorder()
2755 proxy.ServeHTTP(rw, req)
2757 if tc.allowed {
2758 assert.Equal(t, 200, rw.Code)
2759 assert.Equal(t, "Allowed Request", rw.Body.String())
2760 } else {
2761 assert.Equal(t, 403, rw.Code)
2763 })
2767func TestProxyAllowedGroups(t *testing.T) {
2768 tests := []struct {
2769 name string
2770 allowedGroups []string
2771 groups []string
2772 expectUnauthorized bool
2773 }{
2774 {"NoAllowedGroups", []string{}, []string{}, false},
2775 {"NoAllowedGroupsUserHasGroups", []string{}, []string{"a", "b"}, false},
2776 {"UserInAllowedGroup", []string{"a"}, []string{"a", "b"}, false},
2777 {"UserNotInAllowedGroup", []string{"a"}, []string{"c"}, true},
2780 for _, tt := range tests {
2781 t.Run(tt.name, func(t *testing.T) {
2782 emailAddress := "test"
2783 created := time.Now()
2785 session := &sessions.SessionState{
2786 Groups: tt.groups,
2787 Email: emailAddress,
2788 AccessToken: "oauth_token",
2789 CreatedAt: &created,
2792 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2793 w.WriteHeader(200)
2794 }))
2795 t.Cleanup(upstreamServer.Close)
2797 test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
2798 opts.AllowedGroups = tt.allowedGroups
2799 opts.UpstreamServers = options.Upstreams{
2801 ID: upstreamServer.URL,
2802 Path: "/",
2803 URI: upstreamServer.URL,
2804 },
2806 })
2807 if err != nil {
2808 t.Fatal(err)
2811 test.req, _ = http.NewRequest("GET", "/", nil)
2813 test.req.Header.Add("accept", applicationJSON)
2814 err = test.SaveSession(session)
2815 assert.NoError(t, err)
2816 test.proxy.ServeHTTP(test.rw, test.req)
2818 if tt.expectUnauthorized {
2819 assert.Equal(t, http.StatusForbidden, test.rw.Code)
2820 } else {
2821 assert.Equal(t, http.StatusOK, test.rw.Code)
2823 })
2827func TestAuthOnlyAllowedGroups(t *testing.T) {
2828 testCases := []struct {
2829 name string
2830 allowedGroups []string
2831 groups []string
2832 querystring string
2833 expectedStatusCode int
2834 }{
2836 name: "NoAllowedGroups",
2837 allowedGroups: []string{},
2838 groups: []string{},
2839 querystring: "",
2840 expectedStatusCode: http.StatusAccepted,
2841 },
2843 name: "NoAllowedGroupsUserHasGroups",
2844 allowedGroups: []string{},
2845 groups: []string{"a", "b"},
2846 querystring: "",
2847 expectedStatusCode: http.StatusAccepted,
2848 },
2850 name: "UserInAllowedGroup",
2851 allowedGroups: []string{"a"},
2852 groups: []string{"a", "b"},
2853 querystring: "",
2854 expectedStatusCode: http.StatusAccepted,
2855 },
2857 name: "UserNotInAllowedGroup",
2858 allowedGroups: []string{"a"},
2859 groups: []string{"c"},
2860 querystring: "",
2861 expectedStatusCode: http.StatusUnauthorized,
2862 },
2864 name: "UserInQuerystringGroup",
2865 allowedGroups: []string{"a", "b"},
2866 groups: []string{"a", "c"},
2867 querystring: "?allowed_groups=a",
2868 expectedStatusCode: http.StatusAccepted,
2869 },
2871 name: "UserInMultiParamQuerystringGroup",
2872 allowedGroups: []string{"a", "b"},
2873 groups: []string{"b"},
2874 querystring: "?allowed_groups=a&allowed_groups=b,d",
2875 expectedStatusCode: http.StatusAccepted,
2876 },
2878 name: "UserInOnlyQuerystringGroup",
2879 allowedGroups: []string{},
2880 groups: []string{"a", "c"},
2881 querystring: "?allowed_groups=a,b",
2882 expectedStatusCode: http.StatusAccepted,
2883 },
2885 name: "UserInDelimitedQuerystringGroup",
2886 allowedGroups: []string{"a", "b", "c"},
2887 groups: []string{"c"},
2888 querystring: "?allowed_groups=a,c",
2889 expectedStatusCode: http.StatusAccepted,
2890 },
2892 name: "UserNotInQuerystringGroup",
2893 allowedGroups: []string{},
2894 groups: []string{"c"},
2895 querystring: "?allowed_groups=a,b",
2896 expectedStatusCode: http.StatusForbidden,
2897 },
2899 name: "UserInConfigGroupNotInQuerystringGroup",
2900 allowedGroups: []string{"a", "b", "c"},
2901 groups: []string{"c"},
2902 querystring: "?allowed_groups=a,b",
2903 expectedStatusCode: http.StatusForbidden,
2904 },
2906 name: "UserInQuerystringGroupNotInConfigGroup",
2907 allowedGroups: []string{"a", "b"},
2908 groups: []string{"c"},
2909 querystring: "?allowed_groups=b,c",
2910 expectedStatusCode: http.StatusUnauthorized,
2911 },
2914 for _, tc := range testCases {
2915 t.Run(tc.name, func(t *testing.T) {
2916 emailAddress := "test"
2917 created := time.Now()
2919 session := &sessions.SessionState{
2920 Groups: tc.groups,
2921 Email: emailAddress,
2922 AccessToken: "oauth_token",
2923 CreatedAt: &created,
2926 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
2927 opts.AllowedGroups = tc.allowedGroups
2928 })
2929 if err != nil {
2930 t.Fatal(err)
2933 err = test.SaveSession(session)
2934 assert.NoError(t, err)
2936 test.proxy.ServeHTTP(test.rw, test.req)
2938 assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
2939 })