@@ -15,6 +15,7 @@ import (
1515 "time"
1616
1717 "github.com/coreos/go-oidc/v3/oidc"
18+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
1819 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1920 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
2021 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
@@ -23,6 +24,7 @@ import (
2324 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"
2425 sessionscookie "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie"
2526 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
27+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
2628 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
2729 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
2830 "github.com/stretchr/testify/assert"
@@ -52,7 +54,7 @@ func TestRobotsTxt(t *testing.T) {
5254 t.Fatal(err)
5355 }
5456 rw := httptest.NewRecorder()
55− req, _ := http.NewRequest("GET", "/robots.txt", nil)
57+ req, _ := http.NewRequest(http.MethodGet, "/robots.txt", nil)
5658 proxy.ServeHTTP(rw, req)
5759 assert.Equal(t, 200, rw.Code)
5860 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
@@ -239,7 +241,7 @@ func TestBasicAuthPassword(t *testing.T) {
239241
240242 // Save the required session
241243 rw := httptest.NewRecorder()
242− req, _ := http.NewRequest("GET", "/", nil)
244+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
243245 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
244246 Email: emailAddress,
245247 })
@@ -248,7 +250,7 @@ func TestBasicAuthPassword(t *testing.T) {
248250 // Extract the cookie value to inject into the test request
249251 cookie := rw.Header().Values("Set-Cookie")[0]
250252
251− req, _ = http.NewRequest("GET", "/", nil)
253+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
252254 req.Header.Set("Cookie", cookie)
253255 rw = httptest.NewRecorder()
254256 proxy.ServeHTTP(rw, req)
@@ -298,14 +300,14 @@ func TestPassGroupsHeadersWithGroups(t *testing.T) {
298300
299301 // Save the required session
300302 rw := httptest.NewRecorder()
301− req, _ := http.NewRequest("GET", "/", nil)
303+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
302304 err = proxy.sessionStore.Save(rw, req, session)
303305 assert.NoError(t, err)
304306
305307 // Extract the cookie value to inject into the test request
306308 cookie := rw.Header().Values("Set-Cookie")[0]
307309
308− req, _ = http.NewRequest("GET", "/", nil)
310+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
309311 req.Header.Set("Cookie", cookie)
310312 rw = httptest.NewRecorder()
311313 proxy.ServeHTTP(rw, req)
@@ -455,7 +457,7 @@ func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoin
455457 return 0, ""
456458 }
457459
458− req, err := http.NewRequest("GET", endpoint, strings.NewReader(""))
460+ req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
459461 if err != nil {
460462 return 0, ""
461463 }
@@ -506,7 +508,7 @@ func TestStaticProxyUpstream(t *testing.T) {
506508 ProxyUpstream: options.Upstream{
507509 ID: "static-proxy",
508510 Path: "/static-proxy",
509− Static: true,
511+ Static: ptr.To(true),
510512 },
511513 })
512514 if err != nil {
@@ -606,7 +608,7 @@ func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
606608
607609func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
608610 rw := httptest.NewRecorder()
609− req, _ := http.NewRequest("GET", endpoint, strings.NewReader(""))
611+ req, _ := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
610612 sipTest.proxy.ServeHTTP(rw, req)
611613 return rw.Code, rw.Body.String()
612614}
@@ -711,6 +713,50 @@ func TestManualSignInCorrectCredentials(t *testing.T) {
711713 assert.Equal(t, http.StatusFound, statusCode)
712714}
713715
716+func TestSignInPageClearsExistingSessionCookie(t *testing.T) {
717+ opts := baseTestOptions()
718+ err := validation.Validate(opts)
719+ require.NoError(t, err)
720+
721+ proxy, err := NewOAuthProxy(opts, func(string) bool {
722+ return true
723+ })
724+ require.NoError(t, err)
725+
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)
733+
734+ cookies := saveRW.Result().Cookies()
735+ require.NotEmpty(t, cookies)
736+
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+ }
742+
743+ rw := httptest.NewRecorder()
744+ proxy.ServeHTTP(rw, req)
745+
746+ assert.Equal(t, http.StatusOK, rw.Code)
747+
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+ }
756+
757+ assert.True(t, cleared, "expected sign-in page to clear existing session cookie")
758+}
759+
714760func TestSignInPageIncludesTargetRedirect(t *testing.T) {
715761 sipTest, err := NewSignInPageTest(false)
716762 if err != nil {
@@ -848,7 +894,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
848894 // access_token validation.
849895 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
850896 pcTest.rw = httptest.NewRecorder()
851− pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
897+ pcTest.req, _ = http.NewRequest(http.MethodGet, "/", strings.NewReader(""))
852898 pcTest.validateUser = true
853899 return &pcTest, nil
854900}
@@ -981,7 +1027,7 @@ func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
9811027 if err != nil {
9821028 return nil, err
9831029 }
984− pcTest.req, _ = http.NewRequest("GET",
1030+ pcTest.req, _ = http.NewRequest(http.MethodGet,
9851031 pcTest.opts.ProxyPrefix+"/userinfo", nil)
9861032 return pcTest, nil
9871033}
@@ -1031,6 +1077,20 @@ func TestUserInfoEndpointAccepted(t *testing.T) {
10311077 },
10321078 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\"}\n",
10331079 },
1080+ {
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+ },
10341094 }
10351095
10361096 for _, tc := range testCases {
@@ -1075,7 +1135,7 @@ func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (
10751135 return nil, err
10761136 }
10771137 pcTest.req, _ = http.NewRequest(
1078− "GET",
1138+ http.MethodGet,
10791139 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
10801140 nil)
10811141 return pcTest, nil
@@ -1214,7 +1274,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
12141274 pcTest.validateUser = true
12151275
12161276 pcTest.rw = httptest.NewRecorder()
1217− pcTest.req, _ = http.NewRequest("GET",
1277+ pcTest.req, _ = http.NewRequest(http.MethodGet,
12181278 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
12191279
12201280 created := time.Now()
@@ -1307,7 +1367,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
13071367 pcTest.validateUser = true
13081368
13091369 pcTest.rw = httptest.NewRecorder()
1310− pcTest.req, _ = http.NewRequest("GET",
1370+ pcTest.req, _ = http.NewRequest(http.MethodGet,
13111371 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
13121372
13131373 created := time.Now()
@@ -1387,7 +1447,7 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
13871447 pcTest.validateUser = true
13881448
13891449 pcTest.rw = httptest.NewRecorder()
1390− pcTest.req, _ = http.NewRequest("GET",
1450+ pcTest.req, _ = http.NewRequest(http.MethodGet,
13911451 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
13921452
13931453 created := time.Now()
@@ -1435,7 +1495,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
14351495 }
14361496 proxy.provider = NewTestProvider(upstreamURL, "")
14371497 rw := httptest.NewRecorder()
1438− req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil)
1498+ req, _ := http.NewRequest(http.MethodOptions, "/preflight-request", nil)
14391499 proxy.ServeHTTP(rw, req)
14401500
14411501 assert.Equal(t, 200, rw.Code)
@@ -1592,19 +1652,19 @@ func TestRequestSignature(t *testing.T) {
15921652 resp string
15931653 }{
15941654 "No request signature": {
1595− method: "GET",
1655+ method: http.MethodGet,
15961656 body: "",
15971657 key: "",
15981658 resp: "no signature received",
15991659 },
16001660 "Get request": {
1601− method: "GET",
1661+ method: http.MethodGet,
16021662 body: "",
16031663 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
16041664 resp: "signatures match",
16051665 },
16061666 "Post request": {
1607− method: "POST",
1667+ method: http.MethodPost,
16081668 body: `{ "hello": "world!" }`,
16091669 key: "d90df39e2d19282840252612dd7c81421a372f61",
16101670 resp: "signatures match",
@@ -2027,6 +2087,44 @@ func Test_noCacheHeaders(t *testing.T) {
20272087 })
20282088}
20292089
2090+func 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()
2098+
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)
2103+
2104+ proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
2105+ require.NoError(t, err)
2106+
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]
2116+
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)
2122+
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")
2126+}
2127+
20302128func baseTestOptions() *options.Options {
20312129 opts := options.NewOptions()
20322130 opts.Cookie.Secret = rawCookieSecret
@@ -2091,7 +2189,33 @@ func TestTrustedIPs(t *testing.T) {
20912189 reverseProxy: false,
20922190 realClientIPHeader: "X-Real-IP", // Default value
20932191 req: func() *http.Request {
2094− req, _ := http.NewRequest("GET", "/", nil)
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.
2198+ {
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).
2211+ {
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 = "@"
20952219 return req
20962220 }(),
20972221 expectTrusted: false,
@@ -2103,7 +2227,7 @@ func TestTrustedIPs(t *testing.T) {
21032227 reverseProxy: false,
21042228 realClientIPHeader: "X-Real-IP", // Default value
21052229 req: func() *http.Request {
2106− req, _ := http.NewRequest("GET", "/", nil)
2230+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21072231 req.RemoteAddr = "127.0.0.1:43670"
21082232 return req
21092233 }(),
@@ -2116,7 +2240,7 @@ func TestTrustedIPs(t *testing.T) {
21162240 reverseProxy: true,
21172241 realClientIPHeader: "X-Real-IP", // Default value
21182242 req: func() *http.Request {
2119− req, _ := http.NewRequest("GET", "/", nil)
2243+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21202244 req.RemoteAddr = "127.0.0.1:44324"
21212245 return req
21222246 }(),
@@ -2129,7 +2253,7 @@ func TestTrustedIPs(t *testing.T) {
21292253 reverseProxy: true,
21302254 realClientIPHeader: "X-Forwarded-For",
21312255 req: func() *http.Request {
2132− req, _ := http.NewRequest("GET", "/", nil)
2256+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21332257 req.Header.Add("X-Forwarded-For", "127.0.0.1")
21342258 return req
21352259 }(),
@@ -2142,7 +2266,7 @@ func TestTrustedIPs(t *testing.T) {
21422266 reverseProxy: true,
21432267 realClientIPHeader: "X-Forwarded-For",
21442268 req: func() *http.Request {
2145− req, _ := http.NewRequest("GET", "/", nil)
2269+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21462270 req.Header.Add("X-Forwarded-For", "::1")
21472271 return req
21482272 }(),
@@ -2155,7 +2279,7 @@ func TestTrustedIPs(t *testing.T) {
21552279 reverseProxy: true,
21562280 realClientIPHeader: "X-Forwarded-For",
21572281 req: func() *http.Request {
2158− req, _ := http.NewRequest("GET", "/", nil)
2282+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21592283 req.Header.Add("X-Forwarded-For", "12.34.56.78")
21602284 return req
21612285 }(),
@@ -2168,7 +2292,7 @@ func TestTrustedIPs(t *testing.T) {
21682292 reverseProxy: true,
21692293 realClientIPHeader: "X-Forwarded-For",
21702294 req: func() *http.Request {
2171− req, _ := http.NewRequest("GET", "/", nil)
2295+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21722296 req.Header.Add("X-Forwarded-For", "::2")
21732297 return req
21742298 }(),
@@ -2181,7 +2305,7 @@ func TestTrustedIPs(t *testing.T) {
21812305 reverseProxy: true,
21822306 realClientIPHeader: "X-Forwarded-For",
21832307 req: func() *http.Request {
2184− req, _ := http.NewRequest("GET", "/", nil)
2308+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21852309 req.Header.Add("X-Real-IP", "::1")
21862310 return req
21872311 }(),
@@ -2194,7 +2318,7 @@ func TestTrustedIPs(t *testing.T) {
21942318 reverseProxy: true,
21952319 realClientIPHeader: "X-Forwarded-For",
21962320 req: func() *http.Request {
2197− req, _ := http.NewRequest("GET", "/", nil)
2321+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
21982322 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
21992323 return req
22002324 }(),
@@ -2207,7 +2331,7 @@ func TestTrustedIPs(t *testing.T) {
22072331 reverseProxy: false,
22082332 realClientIPHeader: "X-Real-IP",
22092333 req: func() *http.Request {
2210− req, _ := http.NewRequest("GET", "/", nil)
2334+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
22112335 req.RemoteAddr = "adsfljk29242as!!"
22122336 return req
22132337 }(),
@@ -2223,7 +2347,7 @@ func TestTrustedIPs(t *testing.T) {
22232347 {
22242348 ID: "static",
22252349 Path: "/",
2226− Static: true,
2350+ Static: ptr.To(true),
22272351 },
22282352 },
22292353 }
@@ -2303,12 +2427,12 @@ func Test_buildRoutesAllowlist(t *testing.T) {
23032427 },
23042428 expectedRoutes: []expectedAllowedRoute{
23052429 {
2306− method: "GET",
2430+ method: http.MethodGet,
23072431 negate: false,
23082432 regexString: "^/foo/bar",
23092433 },
23102434 {
2311− method: "POST",
2435+ method: http.MethodPost,
23122436 negate: false,
23132437 regexString: "^/baz/[0-9]+/thing",
23142438 },
@@ -2361,11 +2485,11 @@ func Test_buildRoutesAllowlist(t *testing.T) {
23612485 regexString: "^/baz/[0-9]+/thing/regex",
23622486 },
23632487 {
2364− method: "GET",
2488+ method: http.MethodGet,
23652489 regexString: "^/foo/bar",
23662490 },
23672491 {
2368− method: "POST",
2492+ method: http.MethodPost,
23692493 regexString: "^/baz/[0-9]+/thing",
23702494 },
23712495 {
@@ -2517,7 +2641,7 @@ func TestApiRoutes(t *testing.T) {
25172641
25182642 for _, tc := range testCases {
25192643 t.Run(tc.name, func(t *testing.T) {
2520− req, err := http.NewRequest("GET", tc.url, nil)
2644+ req, err := http.NewRequest(http.MethodGet, tc.url, nil)
25212645 req.Header.Set("Accept", tc.contentType)
25222646 assert.NoError(t, err)
25232647
@@ -2555,9 +2679,11 @@ func TestAllowedRequest(t *testing.T) {
25552679 }
25562680 opts.SkipAuthRegex = []string{
25572681 "^/skip/auth/regex$",
2682+ "^/public/.*/endpoint$",
25582683 }
25592684 opts.SkipAuthRoutes = []string{
25602685 "GET=^/skip/auth/routes/get",
2686+ "^/foo/.*/bar$",
25612687 }
25622688 err := validation.Validate(opts)
25632689 assert.NoError(t, err)
@@ -2574,25 +2700,37 @@ func TestAllowedRequest(t *testing.T) {
25742700 }{
25752701 {
25762702 name: "Regex GET allowed",
2577− method: "GET",
2703+ method: http.MethodGet,
25782704 url: "/skip/auth/regex",
25792705 allowed: true,
25802706 },
25812707 {
25822708 name: "Regex POST allowed ",
2583− method: "POST",
2709+ method: http.MethodPost,
25842710 url: "/skip/auth/regex",
25852711 allowed: true,
25862712 },
25872713 {
25882714 name: "Regex denied",
2589− method: "GET",
2715+ method: http.MethodGet,
25902716 url: "/wrong/denied",
25912717 allowed: false,
25922718 },
2719+ {
2720+ name: "Regex allowed with fragment-free path",
2721+ method: http.MethodGet,
2722+ url: "/public/legit/endpoint",
2723+ allowed: true,
2724+ },
2725+ {
2726+ name: "Regex denied when path contains encoded fragment suffix",
2727+ method: http.MethodGet,
2728+ url: "/public/secret%23/endpoint",
2729+ allowed: false,
2730+ },
25932731 {
25942732 name: "Route allowed",
2595− method: "GET",
2733+ method: http.MethodGet,
25962734 url: "/skip/auth/routes/get",
25972735 allowed: true,
25982736 },
@@ -2604,16 +2742,28 @@ func TestAllowedRequest(t *testing.T) {
26042742 },
26052743 {
26062744 name: "Route denied with wrong path",
2607− method: "GET",
2745+ method: http.MethodGet,
26082746 url: "/skip/auth/routes/wrong/path",
26092747 allowed: false,
26102748 },
26112749 {
26122750 name: "Route denied with wrong path and method",
2613− method: "POST",
2751+ method: http.MethodPost,
26142752 url: "/skip/auth/routes/wrong/path",
26152753 allowed: false,
26162754 },
2755+ {
2756+ name: "Route allowed with fragment-free path",
2757+ method: http.MethodGet,
2758+ url: "/foo/public/bar",
2759+ allowed: true,
2760+ },
2761+ {
2762+ name: "Route denied when path contains encoded fragment suffix",
2763+ method: http.MethodGet,
2764+ url: "/foo/secret%23/bar",
2765+ allowed: false,
2766+ },
26172767 }
26182768
26192769 for _, tc := range testCases {
@@ -2654,9 +2804,11 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
26542804 }
26552805 opts.SkipAuthRegex = []string{
26562806 "^/skip/auth/regex$",
2807+ "^/public/.*/endpoint$",
26572808 }
26582809 opts.SkipAuthRoutes = []string{
26592810 "GET=^/skip/auth/routes/get",
2811+ "^/foo/.*/bar$",
26602812 }
26612813 err := validation.Validate(opts)
26622814 assert.NoError(t, err)
@@ -2673,25 +2825,37 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
26732825 }{
26742826 {
26752827 name: "Regex GET allowed",
2676− method: "GET",
2828+ method: http.MethodGet,
26772829 url: "/skip/auth/regex",
26782830 allowed: true,
26792831 },
26802832 {
26812833 name: "Regex POST allowed ",
2682− method: "POST",
2834+ method: http.MethodPost,
26832835 url: "/skip/auth/regex",
26842836 allowed: true,
26852837 },
26862838 {
26872839 name: "Regex denied",
2688− method: "GET",
2840+ method: http.MethodGet,
26892841 url: "/wrong/denied",
26902842 allowed: false,
26912843 },
2844+ {
2845+ name: "Regex allowed with fragment-free path",
2846+ method: http.MethodGet,
2847+ url: "/public/legit/endpoint",
2848+ allowed: true,
2849+ },
2850+ {
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+ },
26922856 {
26932857 name: "Route allowed",
2694− method: "GET",
2858+ method: http.MethodGet,
26952859 url: "/skip/auth/routes/get",
26962860 allowed: true,
26972861 },
@@ -2703,22 +2867,35 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
27032867 },
27042868 {
27052869 name: "Route denied with wrong path",
2706− method: "GET",
2870+ method: http.MethodGet,
27072871 url: "/skip/auth/routes/wrong/path",
27082872 allowed: false,
27092873 },
27102874 {
27112875 name: "Route denied with wrong path and method",
2712− method: "POST",
2876+ method: http.MethodPost,
27132877 url: "/skip/auth/routes/wrong/path",
27142878 allowed: false,
27152879 },
2880+ {
2881+ name: "Route allowed with fragment-free path",
2882+ method: http.MethodGet,
2883+ url: "/foo/public/bar",
2884+ allowed: true,
2885+ },
2886+ {
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+ },
27162892 }
27172893
27182894 for _, tc := range testCases {
27192895 t.Run(tc.name, func(t *testing.T) {
27202896 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
27212897 req.Header.Set("X-Forwarded-Uri", tc.url)
2898+ req.RemoteAddr = "127.0.0.1:4180"
27222899 assert.NoError(t, err)
27232900
27242901 rw := httptest.NewRecorder()
@@ -2733,6 +2910,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
27332910 }
27342911}
27352912
2913+func 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)
2918+
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{
2924+ {
2925+ ID: upstreamServer.URL,
2926+ Path: "/",
2927+ URI: upstreamServer.URL,
2928+ },
2929+ },
2930+ }
2931+ opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
2932+
2933+ err := validation.Validate(opts)
2934+ assert.NoError(t, err)
2935+
2936+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2937+ assert.NoError(t, err)
2938+
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")
2943+
2944+ rw := httptest.NewRecorder()
2945+ proxy.ServeHTTP(rw, req)
2946+
2947+ assert.Equal(t, 401, rw.Code)
2948+}
2949+
27362950func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
27372951 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27382952 w.WriteHeader(200)
@@ -2772,37 +2986,37 @@ func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
27722986 }{
27732987 {
27742988 name: "Some static file allowed",
2775− method: "GET",
2989+ method: http.MethodGet,
27762990 url: "/static/file.txt",
27772991 allowed: true,
27782992 },
27792993 {
27802994 name: "POST to contact form allowed",
2781− method: "POST",
2995+ method: http.MethodPost,
27822996 url: "/contact",
27832997 allowed: true,
27842998 },
27852999 {
27863000 name: "Regex POST allowed",
2787− method: "POST",
3001+ method: http.MethodPost,
27883002 url: "/api/public-entity",
27893003 allowed: true,
27903004 },
27913005 {
27923006 name: "Regex POST with trailing slash allowed",
2793− method: "POST",
3007+ method: http.MethodPost,
27943008 url: "/api/public-entity/",
27953009 allowed: true,
27963010 },
27973011 {
27983012 name: "Regex GET api route denied",
2799− method: "GET",
3013+ method: http.MethodGet,
28003014 url: "/api/users",
28013015 allowed: false,
28023016 },
28033017 {
28043018 name: "Regex POST api route denied",
2805− method: "POST",
3019+ method: http.MethodPost,
28063020 url: "/api/users",
28073021 allowed: false,
28083022 },
@@ -2872,37 +3086,37 @@ func TestAllowedRequestNegateWithMethod(t *testing.T) {
28723086 }{
28733087 {
28743088 name: "Some static file allowed",
2875− method: "GET",
3089+ method: http.MethodGet,
28763090 url: "/static/file.txt",
28773091 allowed: true,
28783092 },
28793093 {
28803094 name: "POST to contact form not allowed",
2881− method: "POST",
3095+ method: http.MethodPost,
28823096 url: "/contact",
28833097 allowed: false,
28843098 },
28853099 {
28863100 name: "Regex POST allowed",
2887− method: "POST",
3101+ method: http.MethodPost,
28883102 url: "/api/public-entity",
28893103 allowed: true,
28903104 },
28913105 {
28923106 name: "Regex POST with trailing slash allowed",
2893− method: "POST",
3107+ method: http.MethodPost,
28943108 url: "/api/public-entity/",
28953109 allowed: true,
28963110 },
28973111 {
28983112 name: "Regex GET api route denied",
2899− method: "GET",
3113+ method: http.MethodGet,
29003114 url: "/api/users",
29013115 allowed: false,
29023116 },
29033117 {
29043118 name: "Regex POST api route denied",
2905− method: "POST",
3119+ method: http.MethodPost,
29063120 url: "/api/users",
29073121 allowed: false,
29083122 },
@@ -3042,7 +3256,7 @@ func TestProxyAllowedGroups(t *testing.T) {
30423256 t.Fatal(err)
30433257 }
30443258
3045− test.req, _ = http.NewRequest("GET", fmt.Sprintf("/%s", tt.querystring), nil)
3259+ test.req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/%s", tt.querystring), nil)
30463260
30473261 test.req.Header.Add("accept", applicationJSON)
30483262 err = test.SaveSession(session)
@@ -3186,7 +3400,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
31863400 {
31873401 name: "UserWithGroupSkipAuthPreflight",
31883402 groups: []string{"a", "c"},
3189− method: "OPTIONS",
3403+ method: http.MethodOptions,
31903404 ip: "1.2.3.5:43670",
31913405 withSession: true,
31923406 expectedStatusCode: http.StatusAccepted,
@@ -3194,7 +3408,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
31943408 {
31953409 name: "UserWithGroupTrustedIp",
31963410 groups: []string{"a", "c"},
3197− method: "GET",
3411+ method: http.MethodGet,
31983412 ip: "1.2.3.4:43670",
31993413 withSession: true,
32003414 expectedStatusCode: http.StatusAccepted,
@@ -3202,7 +3416,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
32023416 {
32033417 name: "UserWithoutGroupSkipAuthPreflight",
32043418 groups: []string{"c"},
3205− method: "OPTIONS",
3419+ method: http.MethodOptions,
32063420 ip: "1.2.3.5:43670",
32073421 withSession: true,
32083422 expectedStatusCode: http.StatusForbidden,
@@ -3210,21 +3424,21 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
32103424 {
32113425 name: "UserWithoutGroupTrustedIp",
32123426 groups: []string{"c"},
3213− method: "GET",
3427+ method: http.MethodGet,
32143428 ip: "1.2.3.4:43670",
32153429 withSession: true,
32163430 expectedStatusCode: http.StatusForbidden,
32173431 },
32183432 {
32193433 name: "UserWithoutSessionSkipAuthPreflight",
3220− method: "OPTIONS",
3434+ method: http.MethodOptions,
32213435 ip: "1.2.3.5:43670",
32223436 withSession: false,
32233437 expectedStatusCode: http.StatusAccepted,
32243438 },
32253439 {
32263440 name: "UserWithoutSessionTrustedIp",
3227− method: "GET",
3441+ method: http.MethodGet,
32283442 ip: "1.2.3.4:43670",
32293443 withSession: false,
32303444 expectedStatusCode: http.StatusAccepted,
@@ -3324,6 +3538,24 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
33243538 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
33253539 expectedStatusCode: http.StatusAccepted,
33263540 },
3541+ {
3542+ name: "UserWithMultipleAtSignsExactDomain",
3543+ email: "attacker@evil.com@example.com",
3544+ querystring: "?allowed_email_domains=example.com",
3545+ expectedStatusCode: http.StatusForbidden,
3546+ },
3547+ {
3548+ name: "UserWithMultipleAtSignsWildcardDomain",
3549+ email: "attacker@evil.com@foo.example.com",
3550+ querystring: "?allowed_email_domains=*.example.com",
3551+ expectedStatusCode: http.StatusForbidden,
3552+ },
3553+ {
3554+ name: "UserWithMultipleAtSignsDotPrefixedDomain",
3555+ email: "attacker@evil.com@foo.example.com",
3556+ querystring: "?allowed_email_domains=.example.com",
3557+ expectedStatusCode: http.StatusForbidden,
3558+ },
33273559 }
33283560
33293561 for _, tc := range testCases {
@@ -3530,3 +3762,49 @@ func TestGetOAuthRedirectURI(t *testing.T) {
35303762 })
35313763 }
35323764}
3765+
3766+func TestIdTokenPlaceholderInSignOut(t *testing.T) {
3767+ opts := baseTestOptions()
3768+ opts.WhitelistDomains = []string{"my-oidc-provider.example.com"}
3769+
3770+ err := validation.Validate(opts)
3771+ assert.NoError(t, err)
3772+
3773+ const emailAddress = "john.doe@example.com"
3774+ const userName = "9fcab5c9b889a557"
3775+ created := time.Now()
3776+
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,
3784+ }
3785+
3786+ proxy, err := NewOAuthProxy(opts, func(email string) bool {
3787+ return true
3788+ })
3789+ assert.NoError(t, err)
3790+
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)
3796+
3797+ rw = httptest.NewRecorder()
3798+
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+ })
3805+
3806+ proxy.SignOut(rw, req)
3807+ newLocation := rw.Header().Values("Location")[0]
3808+
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)
3810+}