@@ -15,6 +15,7 @@ import (
17 "github.com/coreos/go-oidc/v3/oidc"
17 "github.com/coreos/go-oidc/v3/oidc"
18+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
@@ -53,7 +54,7 @@ func TestRobotsTxt(t *testing.T) {
53 t.Fatal(err)
54 t.Fatal(err)
55 rw := httptest.NewRecorder()
56 rw := httptest.NewRecorder()
56− req, _ := http.NewRequest("GET", "/robots.txt", nil)
57+ req, _ := http.NewRequest(http.MethodGet, "/robots.txt", nil)
57 proxy.ServeHTTP(rw, req)
58 proxy.ServeHTTP(rw, req)
58 assert.Equal(t, 200, rw.Code)
59 assert.Equal(t, 200, rw.Code)
59 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
60 assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
@@ -240,7 +241,7 @@ func TestBasicAuthPassword(t *testing.T) {
241 // Save the required session
242 // Save the required session
242 rw := httptest.NewRecorder()
243 rw := httptest.NewRecorder()
243− req, _ := http.NewRequest("GET", "/", nil)
244+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
244 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
245 err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
245 Email: emailAddress,
246 Email: emailAddress,
@@ -249,7 +250,7 @@ func TestBasicAuthPassword(t *testing.T) {
249 // Extract the cookie value to inject into the test request
250 // Extract the cookie value to inject into the test request
250 cookie := rw.Header().Values("Set-Cookie")[0]
251 cookie := rw.Header().Values("Set-Cookie")[0]
252− req, _ = http.NewRequest("GET", "/", nil)
253+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
253 req.Header.Set("Cookie", cookie)
254 req.Header.Set("Cookie", cookie)
254 rw = httptest.NewRecorder()
255 rw = httptest.NewRecorder()
255 proxy.ServeHTTP(rw, req)
256 proxy.ServeHTTP(rw, req)
@@ -299,14 +300,14 @@ func TestPassGroupsHeadersWithGroups(t *testing.T) {
300 // Save the required session
301 // Save the required session
301 rw := httptest.NewRecorder()
302 rw := httptest.NewRecorder()
302− req, _ := http.NewRequest("GET", "/", nil)
303+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
303 err = proxy.sessionStore.Save(rw, req, session)
304 err = proxy.sessionStore.Save(rw, req, session)
304 assert.NoError(t, err)
305 assert.NoError(t, err)
306 // Extract the cookie value to inject into the test request
307 // Extract the cookie value to inject into the test request
307 cookie := rw.Header().Values("Set-Cookie")[0]
308 cookie := rw.Header().Values("Set-Cookie")[0]
309− req, _ = http.NewRequest("GET", "/", nil)
310+ req, _ = http.NewRequest(http.MethodGet, "/", nil)
310 req.Header.Set("Cookie", cookie)
311 req.Header.Set("Cookie", cookie)
311 rw = httptest.NewRecorder()
312 rw = httptest.NewRecorder()
312 proxy.ServeHTTP(rw, req)
313 proxy.ServeHTTP(rw, req)
@@ -456,7 +457,7 @@ func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoin
456 return 0, ""
457 return 0, ""
459− req, err := http.NewRequest("GET", endpoint, strings.NewReader(""))
460+ req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
460 if err != nil {
461 if err != nil {
461 return 0, ""
462 return 0, ""
@@ -607,7 +608,7 @@ func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
608func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
609func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
609 rw := httptest.NewRecorder()
610 rw := httptest.NewRecorder()
610− req, _ := http.NewRequest("GET", endpoint, strings.NewReader(""))
611+ req, _ := http.NewRequest(http.MethodGet, endpoint, strings.NewReader(""))
611 sipTest.proxy.ServeHTTP(rw, req)
612 sipTest.proxy.ServeHTTP(rw, req)
612 return rw.Code, rw.Body.String()
613 return rw.Code, rw.Body.String()
@@ -712,6 +713,50 @@ func TestManualSignInCorrectCredentials(t *testing.T) {
712 assert.Equal(t, http.StatusFound, statusCode)
713 assert.Equal(t, http.StatusFound, statusCode)
716+func TestSignInPageClearsExistingSessionCookie(t *testing.T) {
717+ opts := baseTestOptions()
718+ err := validation.Validate(opts)
719+ require.NoError(t, err)
721+ proxy, err := NewOAuthProxy(opts, func(string) bool {
724+ require.NoError(t, err)
726+ // Create a real session cookie using the actual session store.
727+ saveRW := httptest.NewRecorder()
728+ saveReq := httptest.NewRequest(http.MethodGet, "/", nil)
729+ err = proxy.sessionStore.Save(saveRW, saveReq, &sessions.SessionState{
730+ Email: "john.doe@example.com",
732+ require.NoError(t, err)
734+ cookies := saveRW.Result().Cookies()
735+ require.NotEmpty(t, cookies)
737+ // Send that cookie to the sign-in page.
738+ req := httptest.NewRequest(http.MethodGet, "/oauth2/sign_in", nil)
739+ for _, c := range cookies {
743+ rw := httptest.NewRecorder()
744+ proxy.ServeHTTP(rw, req)
746+ assert.Equal(t, http.StatusOK, rw.Code)
749+ for _, c := range rw.Result().Cookies() {
750+ if c.Name == proxy.CookieOptions.Name {
752+ assert.Equal(t, "", c.Value)
753+ assert.Less(t, c.MaxAge, 0)
757+ assert.True(t, cleared, "expected sign-in page to clear existing session cookie")
715func TestSignInPageIncludesTargetRedirect(t *testing.T) {
760func TestSignInPageIncludesTargetRedirect(t *testing.T) {
716 sipTest, err := NewSignInPageTest(false)
761 sipTest, err := NewSignInPageTest(false)
717 if err != nil {
762 if err != nil {
@@ -834,20 +879,8 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
834 return nil, err
879 return nil, err
836 testProvider := &TestProvider{
881 testProvider := &TestProvider{
837− ProviderData: &providers.ProviderData{
882+ ProviderData: &providers.ProviderData{},
838− ProviderName: "Test Provider",
842− Path: "/oauth/authorize",
844− RedeemURL: &url.URL{
847− Path: "/oauth/token",
850− ValidToken: opts.providerValidateCookieResponse,
883+ ValidToken: opts.providerValidateCookieResponse,
853 groups := pcTest.opts.Providers[0].AllowedGroups
886 groups := pcTest.opts.Providers[0].AllowedGroups
@@ -861,7 +894,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
861 // access_token validation.
894 // access_token validation.
862 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
895 pcTest.proxy.CookieOptions.Refresh = time.Duration(0)
863 pcTest.rw = httptest.NewRecorder()
896 pcTest.rw = httptest.NewRecorder()
864− pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
897+ pcTest.req, _ = http.NewRequest(http.MethodGet, "/", strings.NewReader(""))
865 pcTest.validateUser = true
898 pcTest.validateUser = true
866 return &pcTest, nil
899 return &pcTest, nil
@@ -994,7 +1027,7 @@ func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
994 if err != nil {
1027 if err != nil {
995 return nil, err
1028 return nil, err
997− pcTest.req, _ = http.NewRequest("GET",
1030+ pcTest.req, _ = http.NewRequest(http.MethodGet,
998 pcTest.opts.ProxyPrefix+"/userinfo", nil)
1031 pcTest.opts.ProxyPrefix+"/userinfo", nil)
999 return pcTest, nil
1032 return pcTest, nil
@@ -1044,6 +1077,20 @@ func TestUserInfoEndpointAccepted(t *testing.T) {
1045 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\"}\n",
1078 expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\"}\n",
1081+ name: "With Additional Claim",
1082+ session: &sessions.SessionState{
1084+ PreferredUsername: "john",
1085+ Email: "john.doe@example.com",
1086+ Groups: []string{"example", "groups"},
1087+ AccessToken: "my_access_token",
1088+ AdditionalClaims: map[string]interface{}{
1092+ expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\",\"additionalClaims\":{\"foo\":\"bar\"}}\n",
1049 for _, tc := range testCases {
1096 for _, tc := range testCases {
@@ -1088,7 +1135,7 @@ func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (
1088 return nil, err
1135 return nil, err
1090 pcTest.req, _ = http.NewRequest(
1137 pcTest.req, _ = http.NewRequest(
1091− "GET",
1138+ http.MethodGet,
1092 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1139 fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1094 return pcTest, nil
1141 return pcTest, nil
@@ -1124,35 +1171,6 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1124 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1171 assert.Equal(t, "Unauthorized\n", string(bodyBytes))
1127−// TestAuthOnlyEndpointRedirectWithSkipProviderButton tests that when SkipProviderButton
1128−// is true and no session exists, the /auth endpoint should return a 302 redirect
1129−// to the OAuth provider instead of 401. This is important for nginx auth_request
1130−// architecture where 401 triggers error_page handling which can break the redirect flow.
1131−// See: https://github.com/oauth2-proxy/oauth2-proxy/issues/334
1132−func TestAuthOnlyEndpointRedirectWithSkipProviderButton(t *testing.T) {
1133− test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1134− opts.SkipProviderButton = true
1140− test.proxy.ServeHTTP(test.rw, test.req)
1142− // With SkipProviderButton=true and no session, should return 302 redirect
1143− // to OAuth provider, NOT 401 Unauthorized
1144− assert.Equal(t, http.StatusFound, test.rw.Code)
1145− location := test.rw.Header().Get("Location")
1146− assert.NotEmpty(t, location, "Expected Location header for redirect")
1148− // Verify the redirect points to the OAuth provider's authorize endpoint
1149− // and contains key OAuth parameters
1150− assert.Contains(t, location, "/oauth/authorize", "Expected redirect to OAuth authorize endpoint")
1151− assert.Contains(t, location, "client_id=", "Expected client_id in redirect URL")
1152− assert.Contains(t, location, "redirect_uri=", "Expected redirect_uri in redirect URL")
1153− assert.Contains(t, location, "state=", "Expected state parameter in redirect URL")
1156func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1174func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1157 test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1175 test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1158 opts.Cookie.Expire = time.Duration(24) * time.Hour
1176 opts.Cookie.Expire = time.Duration(24) * time.Hour
@@ -1256,7 +1274,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
1256 pcTest.validateUser = true
1274 pcTest.validateUser = true
1258 pcTest.rw = httptest.NewRecorder()
1276 pcTest.rw = httptest.NewRecorder()
1259− pcTest.req, _ = http.NewRequest("GET",
1277+ pcTest.req, _ = http.NewRequest(http.MethodGet,
1260 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1278 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1262 created := time.Now()
1280 created := time.Now()
@@ -1349,7 +1367,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
1349 pcTest.validateUser = true
1367 pcTest.validateUser = true
1351 pcTest.rw = httptest.NewRecorder()
1369 pcTest.rw = httptest.NewRecorder()
1352− pcTest.req, _ = http.NewRequest("GET",
1370+ pcTest.req, _ = http.NewRequest(http.MethodGet,
1353 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1371 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1355 created := time.Now()
1373 created := time.Now()
@@ -1429,7 +1447,7 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
1429 pcTest.validateUser = true
1447 pcTest.validateUser = true
1431 pcTest.rw = httptest.NewRecorder()
1449 pcTest.rw = httptest.NewRecorder()
1432− pcTest.req, _ = http.NewRequest("GET",
1450+ pcTest.req, _ = http.NewRequest(http.MethodGet,
1433 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1451 pcTest.opts.ProxyPrefix+authOnlyPath, nil)
1435 created := time.Now()
1453 created := time.Now()
@@ -1477,7 +1495,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
1478 proxy.provider = NewTestProvider(upstreamURL, "")
1496 proxy.provider = NewTestProvider(upstreamURL, "")
1479 rw := httptest.NewRecorder()
1497 rw := httptest.NewRecorder()
1480− req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil)
1498+ req, _ := http.NewRequest(http.MethodOptions, "/preflight-request", nil)
1481 proxy.ServeHTTP(rw, req)
1499 proxy.ServeHTTP(rw, req)
1483 assert.Equal(t, 200, rw.Code)
1501 assert.Equal(t, 200, rw.Code)
@@ -1634,19 +1652,19 @@ func TestRequestSignature(t *testing.T) {
1634 resp string
1652 resp string
1636 "No request signature": {
1654 "No request signature": {
1637− method: "GET",
1655+ method: http.MethodGet,
1638 body: "",
1656 body: "",
1639 key: "",
1657 key: "",
1640 resp: "no signature received",
1658 resp: "no signature received",
1642 "Get request": {
1660 "Get request": {
1643− method: "GET",
1661+ method: http.MethodGet,
1644 body: "",
1662 body: "",
1645 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1663 key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1646 resp: "signatures match",
1664 resp: "signatures match",
1648 "Post request": {
1666 "Post request": {
1649− method: "POST",
1667+ method: http.MethodPost,
1650 body: `{ "hello": "world!" }`,
1668 body: `{ "hello": "world!" }`,
1651 key: "d90df39e2d19282840252612dd7c81421a372f61",
1669 key: "d90df39e2d19282840252612dd7c81421a372f61",
1652 resp: "signatures match",
1670 resp: "signatures match",
@@ -2069,6 +2087,44 @@ func Test_noCacheHeaders(t *testing.T) {
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)
2097+ defer backendLogoutServer.Close()
2099+ opts := baseTestOptions()
2100+ opts.Providers[0].BackendLogoutURL = backendLogoutServer.URL + "/logout?id_token_hint={id_token}"
2101+ err := validation.Validate(opts)
2102+ require.NoError(t, err)
2104+ proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
2105+ require.NoError(t, err)
2107+ // Save a session with IDToken so backend logout can use it
2108+ rw := httptest.NewRecorder()
2109+ req := httptest.NewRequest(http.MethodGet, "/", nil)
2110+ err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{
2111+ Email: "user@example.com",
2112+ IDToken: testIDToken,
2114+ require.NoError(t, err)
2115+ cookie := rw.Header().Values("Set-Cookie")[0]
2117+ // Hit sign_out with the session cookie; backend logout should be called before session is cleared
2118+ signOutReq := httptest.NewRequest(http.MethodGet, "/oauth2/sign_out", nil)
2119+ signOutReq.Header.Set("Cookie", cookie)
2120+ rec := httptest.NewRecorder()
2121+ proxy.ServeHTTP(rec, signOutReq)
2123+ assert.Equal(t, http.StatusFound, rec.Code, "sign_out should redirect")
2124+ assert.Contains(t, receivedURL, "id_token_hint="+testIDToken,
2125+ "backend logout URL should have been called with id_token from session")
2072func baseTestOptions() *options.Options {
2128func baseTestOptions() *options.Options {
2073 opts := options.NewOptions()
2129 opts := options.NewOptions()
2074 opts.Cookie.Secret = rawCookieSecret
2130 opts.Cookie.Secret = rawCookieSecret
@@ -2133,7 +2189,33 @@ func TestTrustedIPs(t *testing.T) {
2133 reverseProxy: false,
2189 reverseProxy: false,
2134 realClientIPHeader: "X-Real-IP", // Default value
2190 realClientIPHeader: "X-Real-IP", // Default value
2135 req: func() *http.Request {
2191 req: func() *http.Request {
2136− req, _ := http.NewRequest("GET", "/", nil)
2192+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2195+ expectTrusted: false,
2197+ // Check Unix socket with no trusted IPs configured does not error.
2199+ name: "UnixSocketWithoutTrustedIPs",
2201+ reverseProxy: false,
2202+ realClientIPHeader: "X-Real-IP",
2203+ req: func() *http.Request {
2204+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2205+ req.RemoteAddr = "@"
2208+ expectTrusted: false,
2210+ // Check Unix socket with trusted IPs configured returns false (no IP to match).
2212+ name: "UnixSocketWithTrustedIPs",
2213+ trustedIPs: []string{"127.0.0.1"},
2214+ reverseProxy: false,
2215+ realClientIPHeader: "X-Real-IP",
2216+ req: func() *http.Request {
2217+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2218+ req.RemoteAddr = "@"
2137 return req
2219 return req
2139 expectTrusted: false,
2221 expectTrusted: false,
@@ -2145,7 +2227,7 @@ func TestTrustedIPs(t *testing.T) {
2145 reverseProxy: false,
2227 reverseProxy: false,
2146 realClientIPHeader: "X-Real-IP", // Default value
2228 realClientIPHeader: "X-Real-IP", // Default value
2147 req: func() *http.Request {
2229 req: func() *http.Request {
2148− req, _ := http.NewRequest("GET", "/", nil)
2230+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2149 req.RemoteAddr = "127.0.0.1:43670"
2231 req.RemoteAddr = "127.0.0.1:43670"
2150 return req
2232 return req
@@ -2158,7 +2240,7 @@ func TestTrustedIPs(t *testing.T) {
2158 reverseProxy: true,
2240 reverseProxy: true,
2159 realClientIPHeader: "X-Real-IP", // Default value
2241 realClientIPHeader: "X-Real-IP", // Default value
2160 req: func() *http.Request {
2242 req: func() *http.Request {
2161− req, _ := http.NewRequest("GET", "/", nil)
2243+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2162 req.RemoteAddr = "127.0.0.1:44324"
2244 req.RemoteAddr = "127.0.0.1:44324"
2163 return req
2245 return req
@@ -2171,7 +2253,7 @@ func TestTrustedIPs(t *testing.T) {
2171 reverseProxy: true,
2253 reverseProxy: true,
2172 realClientIPHeader: "X-Forwarded-For",
2254 realClientIPHeader: "X-Forwarded-For",
2173 req: func() *http.Request {
2255 req: func() *http.Request {
2174− req, _ := http.NewRequest("GET", "/", nil)
2256+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2175 req.Header.Add("X-Forwarded-For", "127.0.0.1")
2257 req.Header.Add("X-Forwarded-For", "127.0.0.1")
2176 return req
2258 return req
@@ -2184,7 +2266,7 @@ func TestTrustedIPs(t *testing.T) {
2184 reverseProxy: true,
2266 reverseProxy: true,
2185 realClientIPHeader: "X-Forwarded-For",
2267 realClientIPHeader: "X-Forwarded-For",
2186 req: func() *http.Request {
2268 req: func() *http.Request {
2187− req, _ := http.NewRequest("GET", "/", nil)
2269+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2188 req.Header.Add("X-Forwarded-For", "::1")
2270 req.Header.Add("X-Forwarded-For", "::1")
2189 return req
2271 return req
@@ -2197,7 +2279,7 @@ func TestTrustedIPs(t *testing.T) {
2197 reverseProxy: true,
2279 reverseProxy: true,
2198 realClientIPHeader: "X-Forwarded-For",
2280 realClientIPHeader: "X-Forwarded-For",
2199 req: func() *http.Request {
2281 req: func() *http.Request {
2200− req, _ := http.NewRequest("GET", "/", nil)
2282+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2201 req.Header.Add("X-Forwarded-For", "12.34.56.78")
2283 req.Header.Add("X-Forwarded-For", "12.34.56.78")
2202 return req
2284 return req
@@ -2210,7 +2292,7 @@ func TestTrustedIPs(t *testing.T) {
2210 reverseProxy: true,
2292 reverseProxy: true,
2211 realClientIPHeader: "X-Forwarded-For",
2293 realClientIPHeader: "X-Forwarded-For",
2212 req: func() *http.Request {
2294 req: func() *http.Request {
2213− req, _ := http.NewRequest("GET", "/", nil)
2295+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2214 req.Header.Add("X-Forwarded-For", "::2")
2296 req.Header.Add("X-Forwarded-For", "::2")
2215 return req
2297 return req
@@ -2223,7 +2305,7 @@ func TestTrustedIPs(t *testing.T) {
2223 reverseProxy: true,
2305 reverseProxy: true,
2224 realClientIPHeader: "X-Forwarded-For",
2306 realClientIPHeader: "X-Forwarded-For",
2225 req: func() *http.Request {
2307 req: func() *http.Request {
2226− req, _ := http.NewRequest("GET", "/", nil)
2308+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2227 req.Header.Add("X-Real-IP", "::1")
2309 req.Header.Add("X-Real-IP", "::1")
2228 return req
2310 return req
@@ -2236,7 +2318,7 @@ func TestTrustedIPs(t *testing.T) {
2236 reverseProxy: true,
2318 reverseProxy: true,
2237 realClientIPHeader: "X-Forwarded-For",
2319 realClientIPHeader: "X-Forwarded-For",
2238 req: func() *http.Request {
2320 req: func() *http.Request {
2239− req, _ := http.NewRequest("GET", "/", nil)
2321+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2240 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2322 req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2241 return req
2323 return req
@@ -2249,7 +2331,7 @@ func TestTrustedIPs(t *testing.T) {
2249 reverseProxy: false,
2331 reverseProxy: false,
2250 realClientIPHeader: "X-Real-IP",
2332 realClientIPHeader: "X-Real-IP",
2251 req: func() *http.Request {
2333 req: func() *http.Request {
2252− req, _ := http.NewRequest("GET", "/", nil)
2334+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
2253 req.RemoteAddr = "adsfljk29242as!!"
2335 req.RemoteAddr = "adsfljk29242as!!"
2254 return req
2336 return req
@@ -2345,12 +2427,12 @@ func Test_buildRoutesAllowlist(t *testing.T) {
2346 expectedRoutes: []expectedAllowedRoute{
2428 expectedRoutes: []expectedAllowedRoute{
2348− method: "GET",
2430+ method: http.MethodGet,
2349 negate: false,
2431 negate: false,
2350 regexString: "^/foo/bar",
2432 regexString: "^/foo/bar",
2353− method: "POST",
2435+ method: http.MethodPost,
2354 negate: false,
2436 negate: false,
2355 regexString: "^/baz/[0-9]+/thing",
2437 regexString: "^/baz/[0-9]+/thing",
@@ -2403,11 +2485,11 @@ func Test_buildRoutesAllowlist(t *testing.T) {
2403 regexString: "^/baz/[0-9]+/thing/regex",
2485 regexString: "^/baz/[0-9]+/thing/regex",
2406− method: "GET",
2488+ method: http.MethodGet,
2407 regexString: "^/foo/bar",
2489 regexString: "^/foo/bar",
2410− method: "POST",
2492+ method: http.MethodPost,
2411 regexString: "^/baz/[0-9]+/thing",
2493 regexString: "^/baz/[0-9]+/thing",
@@ -2559,7 +2641,7 @@ func TestApiRoutes(t *testing.T) {
2560 for _, tc := range testCases {
2642 for _, tc := range testCases {
2561 t.Run(tc.name, func(t *testing.T) {
2643 t.Run(tc.name, func(t *testing.T) {
2562− req, err := http.NewRequest("GET", tc.url, nil)
2644+ req, err := http.NewRequest(http.MethodGet, tc.url, nil)
2563 req.Header.Set("Accept", tc.contentType)
2645 req.Header.Set("Accept", tc.contentType)
2564 assert.NoError(t, err)
2646 assert.NoError(t, err)
@@ -2597,9 +2679,11 @@ func TestAllowedRequest(t *testing.T) {
2598 opts.SkipAuthRegex = []string{
2680 opts.SkipAuthRegex = []string{
2599 "^/skip/auth/regex$",
2681 "^/skip/auth/regex$",
2682+ "^/public/.*/endpoint$",
2601 opts.SkipAuthRoutes = []string{
2684 opts.SkipAuthRoutes = []string{
2602 "GET=^/skip/auth/routes/get",
2685 "GET=^/skip/auth/routes/get",
2604 err := validation.Validate(opts)
2688 err := validation.Validate(opts)
2605 assert.NoError(t, err)
2689 assert.NoError(t, err)
@@ -2616,25 +2700,37 @@ func TestAllowedRequest(t *testing.T) {
2618 name: "Regex GET allowed",
2702 name: "Regex GET allowed",
2619− method: "GET",
2703+ method: http.MethodGet,
2620 url: "/skip/auth/regex",
2704 url: "/skip/auth/regex",
2621 allowed: true,
2705 allowed: true,
2624 name: "Regex POST allowed ",
2708 name: "Regex POST allowed ",
2625− method: "POST",
2709+ method: http.MethodPost,
2626 url: "/skip/auth/regex",
2710 url: "/skip/auth/regex",
2627 allowed: true,
2711 allowed: true,
2630 name: "Regex denied",
2714 name: "Regex denied",
2631− method: "GET",
2715+ method: http.MethodGet,
2632 url: "/wrong/denied",
2716 url: "/wrong/denied",
2633 allowed: false,
2717 allowed: false,
2720+ name: "Regex allowed with fragment-free path",
2721+ method: http.MethodGet,
2722+ url: "/public/legit/endpoint",
2726+ name: "Regex denied when path contains encoded fragment suffix",
2727+ method: http.MethodGet,
2728+ url: "/public/secret%23/endpoint",
2636 name: "Route allowed",
2732 name: "Route allowed",
2637− method: "GET",
2733+ method: http.MethodGet,
2638 url: "/skip/auth/routes/get",
2734 url: "/skip/auth/routes/get",
2639 allowed: true,
2735 allowed: true,
@@ -2646,16 +2742,28 @@ func TestAllowedRequest(t *testing.T) {
2648 name: "Route denied with wrong path",
2744 name: "Route denied with wrong path",
2649− method: "GET",
2745+ method: http.MethodGet,
2650 url: "/skip/auth/routes/wrong/path",
2746 url: "/skip/auth/routes/wrong/path",
2651 allowed: false,
2747 allowed: false,
2654 name: "Route denied with wrong path and method",
2750 name: "Route denied with wrong path and method",
2655− method: "POST",
2751+ method: http.MethodPost,
2656 url: "/skip/auth/routes/wrong/path",
2752 url: "/skip/auth/routes/wrong/path",
2657 allowed: false,
2753 allowed: false,
2756+ name: "Route allowed with fragment-free path",
2757+ method: http.MethodGet,
2758+ url: "/foo/public/bar",
2762+ name: "Route denied when path contains encoded fragment suffix",
2763+ method: http.MethodGet,
2764+ url: "/foo/secret%23/bar",
2661 for _, tc := range testCases {
2769 for _, tc := range testCases {
@@ -2696,9 +2804,11 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2697 opts.SkipAuthRegex = []string{
2805 opts.SkipAuthRegex = []string{
2698 "^/skip/auth/regex$",
2806 "^/skip/auth/regex$",
2807+ "^/public/.*/endpoint$",
2700 opts.SkipAuthRoutes = []string{
2809 opts.SkipAuthRoutes = []string{
2701 "GET=^/skip/auth/routes/get",
2810 "GET=^/skip/auth/routes/get",
2703 err := validation.Validate(opts)
2813 err := validation.Validate(opts)
2704 assert.NoError(t, err)
2814 assert.NoError(t, err)
@@ -2715,25 +2825,37 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2717 name: "Regex GET allowed",
2827 name: "Regex GET allowed",
2718− method: "GET",
2828+ method: http.MethodGet,
2719 url: "/skip/auth/regex",
2829 url: "/skip/auth/regex",
2720 allowed: true,
2830 allowed: true,
2723 name: "Regex POST allowed ",
2833 name: "Regex POST allowed ",
2724− method: "POST",
2834+ method: http.MethodPost,
2725 url: "/skip/auth/regex",
2835 url: "/skip/auth/regex",
2726 allowed: true,
2836 allowed: true,
2729 name: "Regex denied",
2839 name: "Regex denied",
2730− method: "GET",
2840+ method: http.MethodGet,
2731 url: "/wrong/denied",
2841 url: "/wrong/denied",
2732 allowed: false,
2842 allowed: false,
2845+ name: "Regex allowed with fragment-free path",
2846+ method: http.MethodGet,
2847+ url: "/public/legit/endpoint",
2851+ name: "Regex denied when X-Forwarded-Uri contains an encoded fragment suffix",
2852+ method: http.MethodGet,
2853+ url: "/public/secret%23/endpoint",
2735 name: "Route allowed",
2857 name: "Route allowed",
2736− method: "GET",
2858+ method: http.MethodGet,
2737 url: "/skip/auth/routes/get",
2859 url: "/skip/auth/routes/get",
2738 allowed: true,
2860 allowed: true,
@@ -2745,22 +2867,35 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2747 name: "Route denied with wrong path",
2869 name: "Route denied with wrong path",
2748− method: "GET",
2870+ method: http.MethodGet,
2749 url: "/skip/auth/routes/wrong/path",
2871 url: "/skip/auth/routes/wrong/path",
2750 allowed: false,
2872 allowed: false,
2753 name: "Route denied with wrong path and method",
2875 name: "Route denied with wrong path and method",
2754− method: "POST",
2876+ method: http.MethodPost,
2755 url: "/skip/auth/routes/wrong/path",
2877 url: "/skip/auth/routes/wrong/path",
2756 allowed: false,
2878 allowed: false,
2881+ name: "Route allowed with fragment-free path",
2882+ method: http.MethodGet,
2883+ url: "/foo/public/bar",
2887+ name: "Route denied when X-Forwarded-Uri contains an encoded fragment suffix",
2888+ method: http.MethodGet,
2889+ url: "/foo/secret%23/bar",
2760 for _, tc := range testCases {
2894 for _, tc := range testCases {
2761 t.Run(tc.name, func(t *testing.T) {
2895 t.Run(tc.name, func(t *testing.T) {
2762 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
2896 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
2763 req.Header.Set("X-Forwarded-Uri", tc.url)
2897 req.Header.Set("X-Forwarded-Uri", tc.url)
2898+ req.RemoteAddr = "127.0.0.1:4180"
2764 assert.NoError(t, err)
2899 assert.NoError(t, err)
2766 rw := httptest.NewRecorder()
2901 rw := httptest.NewRecorder()
@@ -2775,6 +2910,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
2913+func TestAllowedRequestWithForwardedUriHeaderRequiresTrustedProxy(t *testing.T) {
2914+ upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2917+ t.Cleanup(upstreamServer.Close)
2919+ opts := baseTestOptions()
2920+ opts.ReverseProxy = true
2921+ opts.TrustedProxyIPs = []string{"127.0.0.1/32"}
2922+ opts.UpstreamServers = options.UpstreamConfig{
2923+ Upstreams: []options.Upstream{
2925+ ID: upstreamServer.URL,
2927+ URI: upstreamServer.URL,
2931+ opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
2933+ err := validation.Validate(opts)
2934+ assert.NoError(t, err)
2936+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2937+ assert.NoError(t, err)
2939+ req, err := http.NewRequest(http.MethodGet, opts.ProxyPrefix+authOnlyPath, nil)
2940+ assert.NoError(t, err)
2941+ req.RemoteAddr = "192.0.2.10:4180"
2942+ req.Header.Set("X-Forwarded-Uri", "/skip/auth/regex")
2944+ rw := httptest.NewRecorder()
2945+ proxy.ServeHTTP(rw, req)
2947+ assert.Equal(t, 401, rw.Code)
2778func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2950func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2779 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2951 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2780 w.WriteHeader(200)
2952 w.WriteHeader(200)
@@ -2814,37 +2986,37 @@ func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
2816 name: "Some static file allowed",
2988 name: "Some static file allowed",
2817− method: "GET",
2989+ method: http.MethodGet,
2818 url: "/static/file.txt",
2990 url: "/static/file.txt",
2819 allowed: true,
2991 allowed: true,
2822 name: "POST to contact form allowed",
2994 name: "POST to contact form allowed",
2823− method: "POST",
2995+ method: http.MethodPost,
2824 url: "/contact",
2996 url: "/contact",
2825 allowed: true,
2997 allowed: true,
2828 name: "Regex POST allowed",
3000 name: "Regex POST allowed",
2829− method: "POST",
3001+ method: http.MethodPost,
2830 url: "/api/public-entity",
3002 url: "/api/public-entity",
2831 allowed: true,
3003 allowed: true,
2834 name: "Regex POST with trailing slash allowed",
3006 name: "Regex POST with trailing slash allowed",
2835− method: "POST",
3007+ method: http.MethodPost,
2836 url: "/api/public-entity/",
3008 url: "/api/public-entity/",
2837 allowed: true,
3009 allowed: true,
2840 name: "Regex GET api route denied",
3012 name: "Regex GET api route denied",
2841− method: "GET",
3013+ method: http.MethodGet,
2842 url: "/api/users",
3014 url: "/api/users",
2843 allowed: false,
3015 allowed: false,
2846 name: "Regex POST api route denied",
3018 name: "Regex POST api route denied",
2847− method: "POST",
3019+ method: http.MethodPost,
2848 url: "/api/users",
3020 url: "/api/users",
2849 allowed: false,
3021 allowed: false,
@@ -2914,37 +3086,37 @@ func TestAllowedRequestNegateWithMethod(t *testing.T) {
2916 name: "Some static file allowed",
3088 name: "Some static file allowed",
2917− method: "GET",
3089+ method: http.MethodGet,
2918 url: "/static/file.txt",
3090 url: "/static/file.txt",
2919 allowed: true,
3091 allowed: true,
2922 name: "POST to contact form not allowed",
3094 name: "POST to contact form not allowed",
2923− method: "POST",
3095+ method: http.MethodPost,
2924 url: "/contact",
3096 url: "/contact",
2925 allowed: false,
3097 allowed: false,
2928 name: "Regex POST allowed",
3100 name: "Regex POST allowed",
2929− method: "POST",
3101+ method: http.MethodPost,
2930 url: "/api/public-entity",
3102 url: "/api/public-entity",
2931 allowed: true,
3103 allowed: true,
2934 name: "Regex POST with trailing slash allowed",
3106 name: "Regex POST with trailing slash allowed",
2935− method: "POST",
3107+ method: http.MethodPost,
2936 url: "/api/public-entity/",
3108 url: "/api/public-entity/",
2937 allowed: true,
3109 allowed: true,
2940 name: "Regex GET api route denied",
3112 name: "Regex GET api route denied",
2941− method: "GET",
3113+ method: http.MethodGet,
2942 url: "/api/users",
3114 url: "/api/users",
2943 allowed: false,
3115 allowed: false,
2946 name: "Regex POST api route denied",
3118 name: "Regex POST api route denied",
2947− method: "POST",
3119+ method: http.MethodPost,
2948 url: "/api/users",
3120 url: "/api/users",
2949 allowed: false,
3121 allowed: false,
@@ -3084,7 +3256,7 @@ func TestProxyAllowedGroups(t *testing.T) {
3084 t.Fatal(err)
3256 t.Fatal(err)
3087− test.req, _ = http.NewRequest("GET", fmt.Sprintf("/%s", tt.querystring), nil)
3259+ test.req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/%s", tt.querystring), nil)
3089 test.req.Header.Add("accept", applicationJSON)
3261 test.req.Header.Add("accept", applicationJSON)
3090 err = test.SaveSession(session)
3262 err = test.SaveSession(session)
@@ -3228,7 +3400,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3229 name: "UserWithGroupSkipAuthPreflight",
3401 name: "UserWithGroupSkipAuthPreflight",
3230 groups: []string{"a", "c"},
3402 groups: []string{"a", "c"},
3231− method: "OPTIONS",
3403+ method: http.MethodOptions,
3232 ip: "1.2.3.5:43670",
3404 ip: "1.2.3.5:43670",
3233 withSession: true,
3405 withSession: true,
3234 expectedStatusCode: http.StatusAccepted,
3406 expectedStatusCode: http.StatusAccepted,
@@ -3236,7 +3408,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3237 name: "UserWithGroupTrustedIp",
3409 name: "UserWithGroupTrustedIp",
3238 groups: []string{"a", "c"},
3410 groups: []string{"a", "c"},
3239− method: "GET",
3411+ method: http.MethodGet,
3240 ip: "1.2.3.4:43670",
3412 ip: "1.2.3.4:43670",
3241 withSession: true,
3413 withSession: true,
3242 expectedStatusCode: http.StatusAccepted,
3414 expectedStatusCode: http.StatusAccepted,
@@ -3244,7 +3416,7 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3245 name: "UserWithoutGroupSkipAuthPreflight",
3417 name: "UserWithoutGroupSkipAuthPreflight",
3246 groups: []string{"c"},
3418 groups: []string{"c"},
3247− method: "OPTIONS",
3419+ method: http.MethodOptions,
3248 ip: "1.2.3.5:43670",
3420 ip: "1.2.3.5:43670",
3249 withSession: true,
3421 withSession: true,
3250 expectedStatusCode: http.StatusForbidden,
3422 expectedStatusCode: http.StatusForbidden,
@@ -3252,21 +3424,21 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
3253 name: "UserWithoutGroupTrustedIp",
3425 name: "UserWithoutGroupTrustedIp",
3254 groups: []string{"c"},
3426 groups: []string{"c"},
3255− method: "GET",
3427+ method: http.MethodGet,
3256 ip: "1.2.3.4:43670",
3428 ip: "1.2.3.4:43670",
3257 withSession: true,
3429 withSession: true,
3258 expectedStatusCode: http.StatusForbidden,
3430 expectedStatusCode: http.StatusForbidden,
3261 name: "UserWithoutSessionSkipAuthPreflight",
3433 name: "UserWithoutSessionSkipAuthPreflight",
3262− method: "OPTIONS",
3434+ method: http.MethodOptions,
3263 ip: "1.2.3.5:43670",
3435 ip: "1.2.3.5:43670",
3264 withSession: false,
3436 withSession: false,
3265 expectedStatusCode: http.StatusAccepted,
3437 expectedStatusCode: http.StatusAccepted,
3268 name: "UserWithoutSessionTrustedIp",
3440 name: "UserWithoutSessionTrustedIp",
3269− method: "GET",
3441+ method: http.MethodGet,
3270 ip: "1.2.3.4:43670",
3442 ip: "1.2.3.4:43670",
3271 withSession: false,
3443 withSession: false,
3272 expectedStatusCode: http.StatusAccepted,
3444 expectedStatusCode: http.StatusAccepted,
@@ -3366,6 +3538,24 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
3366 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
3538 querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
3367 expectedStatusCode: http.StatusAccepted,
3539 expectedStatusCode: http.StatusAccepted,
3542+ name: "UserWithMultipleAtSignsExactDomain",
3543+ email: "attacker@evil.com@example.com",
3544+ querystring: "?allowed_email_domains=example.com",
3545+ expectedStatusCode: http.StatusForbidden,
3548+ name: "UserWithMultipleAtSignsWildcardDomain",
3549+ email: "attacker@evil.com@foo.example.com",
3550+ querystring: "?allowed_email_domains=*.example.com",
3551+ expectedStatusCode: http.StatusForbidden,
3554+ name: "UserWithMultipleAtSignsDotPrefixedDomain",
3555+ email: "attacker@evil.com@foo.example.com",
3556+ querystring: "?allowed_email_domains=.example.com",
3557+ expectedStatusCode: http.StatusForbidden,
3371 for _, tc := range testCases {
3561 for _, tc := range testCases {
@@ -3572,3 +3762,49 @@ func TestGetOAuthRedirectURI(t *testing.T) {
3766+func TestIdTokenPlaceholderInSignOut(t *testing.T) {
3767+ opts := baseTestOptions()
3768+ opts.WhitelistDomains = []string{"my-oidc-provider.example.com"}
3770+ err := validation.Validate(opts)
3771+ assert.NoError(t, err)
3773+ const emailAddress = "john.doe@example.com"
3774+ const userName = "9fcab5c9b889a557"
3775+ created := time.Now()
3777+ session := &sessions.SessionState{
3779+ Groups: []string{"a", "b"},
3780+ Email: emailAddress,
3781+ IDToken: "eYjjjjjj.vvvv.ddd",
3782+ AccessToken: "oauth_token",
3783+ CreatedAt: &created,
3786+ proxy, err := NewOAuthProxy(opts, func(email string) bool {
3789+ assert.NoError(t, err)
3791+ // Save the required session
3792+ rw := httptest.NewRecorder()
3793+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
3794+ err = proxy.sessionStore.Save(rw, req, session)
3795+ assert.NoError(t, err)
3797+ rw = httptest.NewRecorder()
3799+ rdUrl := url.QueryEscape("https://my-oidc-provider.example.com/sign_out_page?id_token_hint={id_token}&post_logout_redirect_uri=https://my-app.example.com/")
3800+ req, _ = http.NewRequest(http.MethodGet, "/oauth2/sign_out?rd="+rdUrl, nil)
3801+ req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
3802+ RequestID: "11111111-2222-4333-8444-555555555555",
3806+ proxy.SignOut(rw, req)
3807+ newLocation := rw.Header().Values("Location")[0]
3809+ assert.Equal(t, "https://my-oidc-provider.example.com/sign_out_page?id_token_hint=eYjjjjjj.vvvv.ddd&post_logout_redirect_uri=https://my-app.example.com/", newLocation)