Support negating for skip auth routes
This commit is contained in:
parent
037cb041d3
commit
f53754808b
@ -26,7 +26,6 @@ N/A
|
|||||||
|
|
||||||
- [#1774](https://github.com/oauth2-proxy/oauth2-proxy/pull/1774) Fix vulnerabilities CVE-2022-27191, CVE-2021-44716 and CVE-2022-29526
|
- [#1774](https://github.com/oauth2-proxy/oauth2-proxy/pull/1774) Fix vulnerabilities CVE-2022-27191, CVE-2021-44716 and CVE-2022-29526
|
||||||
|
|
||||||
|
|
||||||
- [#1667](https://github.com/oauth2-proxy/oauth2-proxy/issues/1667) Rename configuration file flag for PKCE
|
- [#1667](https://github.com/oauth2-proxy/oauth2-proxy/issues/1667) Rename configuration file flag for PKCE
|
||||||
to remain consistent with CLI flags. You should specify `code_challenge_method` in your configuration instead of
|
to remain consistent with CLI flags. You should specify `code_challenge_method` in your configuration instead of
|
||||||
`force_code_challenge_method`.
|
`force_code_challenge_method`.
|
||||||
@ -36,6 +35,8 @@ to remain consistent with CLI flags. You should specify `code_challenge_method`
|
|||||||
This feature allows parallel callbacks and by default it is disabled.
|
This feature allows parallel callbacks and by default it is disabled.
|
||||||
- Add flag "--cookie-csrf-expire" to define a different expiration time for the CSRF cookie. By default, it is 15 minutes.
|
- Add flag "--cookie-csrf-expire" to define a different expiration time for the CSRF cookie. By default, it is 15 minutes.
|
||||||
|
|
||||||
|
- [#1762](https://github.com/oauth2-proxy/oauth2-proxy/pull/1762) Support negating for skip auth routes
|
||||||
|
|
||||||
# V7.3.0
|
# V7.3.0
|
||||||
|
|
||||||
## Release Highlights
|
## Release Highlights
|
||||||
|
@ -186,7 +186,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
|
|||||||
| `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
|
| `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
|
||||||
| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
|
| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
|
||||||
| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
|
| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
|
||||||
| `--skip-auth-route` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR path_regex alone for all methods | |
|
| `--skip-auth-route` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex | |
|
||||||
| `--skip-auth-strip-headers` | bool | strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy | true |
|
| `--skip-auth-strip-headers` | bool | strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy | true |
|
||||||
| `--skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens (the token must have [`aud`](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields) that matches this client id or one of the extras from `extra-jwt-issuers`) | false |
|
| `--skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens (the token must have [`aud`](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields) that matches this client id or one of the extras from `extra-jwt-issuers`) | false |
|
||||||
| `--skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `--login-url`, `--redeem-url` and `--oidc-jwks-url` must be configured in this case | false |
|
| `--skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `--login-url`, `--redeem-url` and `--oidc-jwks-url` must be configured in this case | false |
|
||||||
|
@ -64,6 +64,7 @@ var (
|
|||||||
// allowedRoute manages method + path based allowlists
|
// allowedRoute manages method + path based allowlists
|
||||||
type allowedRoute struct {
|
type allowedRoute struct {
|
||||||
method string
|
method string
|
||||||
|
negate bool
|
||||||
pathRegex *regexp.Regexp
|
pathRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,9 +446,10 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
|
|||||||
var (
|
var (
|
||||||
method string
|
method string
|
||||||
path string
|
path string
|
||||||
|
negate = strings.Contains(methodPath, "!=")
|
||||||
)
|
)
|
||||||
|
|
||||||
parts := strings.SplitN(methodPath, "=", 2)
|
parts := regexp.MustCompile("!?=").Split(methodPath, 2)
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
method = ""
|
method = ""
|
||||||
path = parts[0]
|
path = parts[0]
|
||||||
@ -463,6 +465,7 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
|
|||||||
logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
|
logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
|
||||||
routes = append(routes, allowedRoute{
|
routes = append(routes, allowedRoute{
|
||||||
method: method,
|
method: method,
|
||||||
|
negate: negate,
|
||||||
pathRegex: compiledRegex,
|
pathRegex: compiledRegex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -516,10 +519,24 @@ func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
|
|||||||
return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
|
return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAllowedMethod(req *http.Request, route allowedRoute) bool {
|
||||||
|
return route.method == "" || req.Method == route.method
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowedPath(req *http.Request, route allowedRoute) bool {
|
||||||
|
matches := route.pathRegex.MatchString(req.URL.Path)
|
||||||
|
|
||||||
|
if route.negate {
|
||||||
|
return !matches
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
// IsAllowedRoute is used to check if the request method & path is allowed without auth
|
// IsAllowedRoute is used to check if the request method & path is allowed without auth
|
||||||
func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
|
func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
|
||||||
for _, route := range p.allowedRoutes {
|
for _, route := range p.allowedRoutes {
|
||||||
if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
|
if isAllowedMethod(req, route) && isAllowedPath(req, route) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2248,6 +2248,7 @@ func TestTrustedIPs(t *testing.T) {
|
|||||||
func Test_buildRoutesAllowlist(t *testing.T) {
|
func Test_buildRoutesAllowlist(t *testing.T) {
|
||||||
type expectedAllowedRoute struct {
|
type expectedAllowedRoute struct {
|
||||||
method string
|
method string
|
||||||
|
negate bool
|
||||||
regexString string
|
regexString string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2275,10 +2276,12 @@ func Test_buildRoutesAllowlist(t *testing.T) {
|
|||||||
expectedRoutes: []expectedAllowedRoute{
|
expectedRoutes: []expectedAllowedRoute{
|
||||||
{
|
{
|
||||||
method: "",
|
method: "",
|
||||||
|
negate: false,
|
||||||
regexString: "^/foo/bar",
|
regexString: "^/foo/bar",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "",
|
method: "",
|
||||||
|
negate: false,
|
||||||
regexString: "^/baz/[0-9]+/thing",
|
regexString: "^/baz/[0-9]+/thing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2293,28 +2296,45 @@ func Test_buildRoutesAllowlist(t *testing.T) {
|
|||||||
"^/all/methods$",
|
"^/all/methods$",
|
||||||
"WEIRD=^/methods/are/allowed",
|
"WEIRD=^/methods/are/allowed",
|
||||||
"PATCH=/second/equals?are=handled&just=fine",
|
"PATCH=/second/equals?are=handled&just=fine",
|
||||||
|
"!=^/api",
|
||||||
|
"METHOD!=^/api",
|
||||||
},
|
},
|
||||||
expectedRoutes: []expectedAllowedRoute{
|
expectedRoutes: []expectedAllowedRoute{
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
negate: false,
|
||||||
regexString: "^/foo/bar",
|
regexString: "^/foo/bar",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
negate: false,
|
||||||
regexString: "^/baz/[0-9]+/thing",
|
regexString: "^/baz/[0-9]+/thing",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "",
|
method: "",
|
||||||
|
negate: false,
|
||||||
regexString: "^/all/methods$",
|
regexString: "^/all/methods$",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "WEIRD",
|
method: "WEIRD",
|
||||||
|
negate: false,
|
||||||
regexString: "^/methods/are/allowed",
|
regexString: "^/methods/are/allowed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
negate: false,
|
||||||
regexString: "/second/equals?are=handled&just=fine",
|
regexString: "/second/equals?are=handled&just=fine",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: "",
|
||||||
|
negate: true,
|
||||||
|
regexString: "^/api",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "METHOD",
|
||||||
|
negate: true,
|
||||||
|
regexString: "^/api",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
},
|
},
|
||||||
@ -2394,6 +2414,7 @@ func Test_buildRoutesAllowlist(t *testing.T) {
|
|||||||
for i, route := range routes {
|
for i, route := range routes {
|
||||||
assert.Greater(t, len(tc.expectedRoutes), i)
|
assert.Greater(t, len(tc.expectedRoutes), i)
|
||||||
assert.Equal(t, route.method, tc.expectedRoutes[i].method)
|
assert.Equal(t, route.method, tc.expectedRoutes[i].method)
|
||||||
|
assert.Equal(t, route.negate, tc.expectedRoutes[i].negate)
|
||||||
assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString)
|
assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -2475,6 +2496,212 @@ func TestAllowedRequest(t *testing.T) {
|
|||||||
url: "/skip/auth/routes/wrong/path",
|
url: "/skip/auth/routes/wrong/path",
|
||||||
allowed: false,
|
allowed: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Route denied with wrong path and method",
|
||||||
|
method: "POST",
|
||||||
|
url: "/skip/auth/routes/wrong/path",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(tc.method, tc.url, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req))
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
proxy.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
if tc.allowed {
|
||||||
|
assert.Equal(t, 200, rw.Code)
|
||||||
|
assert.Equal(t, "Allowed Request", rw.Body.String())
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, 403, rw.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
|
||||||
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
_, err := w.Write([]byte("Allowed Request"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(upstreamServer.Close)
|
||||||
|
|
||||||
|
opts := baseTestOptions()
|
||||||
|
opts.UpstreamServers = options.UpstreamConfig{
|
||||||
|
Upstreams: []options.Upstream{
|
||||||
|
{
|
||||||
|
ID: upstreamServer.URL,
|
||||||
|
Path: "/",
|
||||||
|
URI: upstreamServer.URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
opts.SkipAuthRoutes = []string{
|
||||||
|
"!=^/api", // any non-api routes
|
||||||
|
"POST=^/api/public-entity/?$",
|
||||||
|
}
|
||||||
|
err := validation.Validate(opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
url string
|
||||||
|
allowed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Some static file allowed",
|
||||||
|
method: "GET",
|
||||||
|
url: "/static/file.txt",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST to contact form allowed",
|
||||||
|
method: "POST",
|
||||||
|
url: "/contact",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex POST allowed",
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/public-entity",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex POST with trailing slash allowed",
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/public-entity/",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex GET api route denied",
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/users",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex POST api route denied",
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/users",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex DELETE api route denied",
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/api/users/1",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(tc.method, tc.url, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req))
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
proxy.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
if tc.allowed {
|
||||||
|
assert.Equal(t, 200, rw.Code)
|
||||||
|
assert.Equal(t, "Allowed Request", rw.Body.String())
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, 403, rw.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowedRequestNegateWithMethod(t *testing.T) {
|
||||||
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
_, err := w.Write([]byte("Allowed Request"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(upstreamServer.Close)
|
||||||
|
|
||||||
|
opts := baseTestOptions()
|
||||||
|
opts.UpstreamServers = options.UpstreamConfig{
|
||||||
|
Upstreams: []options.Upstream{
|
||||||
|
{
|
||||||
|
ID: upstreamServer.URL,
|
||||||
|
Path: "/",
|
||||||
|
URI: upstreamServer.URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
opts.SkipAuthRoutes = []string{
|
||||||
|
"GET!=^/api", // any non-api routes
|
||||||
|
"POST=^/api/public-entity/?$",
|
||||||
|
}
|
||||||
|
err := validation.Validate(opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
url string
|
||||||
|
allowed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Some static file allowed",
|
||||||
|
method: "GET",
|
||||||
|
url: "/static/file.txt",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST to contact form not allowed",
|
||||||
|
method: "POST",
|
||||||
|
url: "/contact",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex POST allowed",
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/public-entity",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex POST with trailing slash allowed",
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/public-entity/",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex GET api route denied",
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/users",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex POST api route denied",
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/users",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Regex DELETE api route denied",
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/api/users/1",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -115,7 +115,7 @@ func NewFlagSet() *pflag.FlagSet {
|
|||||||
flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
|
flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
|
||||||
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
|
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
|
||||||
flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)")
|
flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)")
|
||||||
flagSet.StringSlice("skip-auth-route", []string{}, "bypass authentication for requests that match the method & path. Format: method=path_regex OR path_regex alone for all methods")
|
flagSet.StringSlice("skip-auth-route", []string{}, "bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex")
|
||||||
flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
|
flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
|
||||||
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
|
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
|
||||||
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
|
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user