Merge c0784b916c into c0a087d7f2
				
					
				
			This commit is contained in:
		
						commit
						ec7229d0d9
					
				|  | @ -212,7 +212,7 @@ Provider specific options can be found on their respective subpages. | ||||||
| | flag: `--signature-key`<br/>toml: `signature_key`                             | string         | GAP-Signature request signature key (algorithm:secretkey)                                                                                                                                                                                                                                                                                                                                                                                                                                                             |             | | | flag: `--signature-key`<br/>toml: `signature_key`                             | string         | GAP-Signature request signature key (algorithm:secretkey)                                                                                                                                                                                                                                                                                                                                                                                                                                                             |             | | ||||||
| | flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight`                 | bool           | will skip authentication for OPTIONS requests                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | false       | | | flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight`                 | bool           | will skip authentication for OPTIONS requests                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | false       | | ||||||
| | flag: `--skip-auth-regex`<br/>toml: `skip_auth_regex`                         | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times)                                                                                                                                                                                                                                                                                                                                                                                                |             | | | flag: `--skip-auth-regex`<br/>toml: `skip_auth_regex`                         | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times)                                                                                                                                                                                                                                                                                                                                                                                                |             | | ||||||
| | flag: `--skip-auth-route`<br/>toml: `skip_auth_routes`                        | 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                                                                                                                                                                                                                                                                                                                                                         |             | | | flag: `--skip-auth-route`<br/>toml: `skip_auth_routes`                        | string \| list | bypass authentication for requests that match the method, path, or domain. Format: method=path_regex OR method!=path_regex OR domain=domain_regex. For all methods: path_regex OR !=path_regex. Domain matching uses the Host header (or X-Forwarded-Host if behind a reverse proxy).                                                                                                                                                                                                                                   |             | | ||||||
| | flag: `--skip-jwt-bearer-tokens`<br/>toml: `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       | | | flag: `--skip-jwt-bearer-tokens`<br/>toml: `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       | | ||||||
| | flag: `--skip-provider-button`<br/>toml: `skip_provider_button`               | bool           | will skip sign-in-page to directly reach the next step: oauth/start                                                                                                                                                                                                                                                                                                                                                                                                                                                   | false       | | | flag: `--skip-provider-button`<br/>toml: `skip_provider_button`               | bool           | will skip sign-in-page to directly reach the next step: oauth/start                                                                                                                                                                                                                                                                                                                                                                                                                                                   | false       | | ||||||
| | flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify`       | bool           | skip validation of certificates presented when using HTTPS providers                                                                                                                                                                                                                                                                                                                                                                                                                                                  | false       | | | flag: `--ssl-insecure-skip-verify`<br/>toml: `ssl_insecure_skip_verify`       | bool           | skip validation of certificates presented when using HTTPS providers                                                                                                                                                                                                                                                                                                                                                                                                                                                  | false       | | ||||||
|  |  | ||||||
|  | @ -67,11 +67,12 @@ var ( | ||||||
| 	staticFiles embed.FS | 	staticFiles embed.FS | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // allowedRoute manages method + path based allowlists
 | // allowedRoute manages method + path + domain based allowlists
 | ||||||
| type allowedRoute struct { | type allowedRoute struct { | ||||||
| 	method      string | 	method      string | ||||||
| 	negate      bool | 	negate      bool | ||||||
| 	pathRegex   *regexp.Regexp | 	pathRegex   *regexp.Regexp | ||||||
|  | 	domainRegex *regexp.Regexp | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiRoute struct { | type apiRoute struct { | ||||||
|  | @ -464,7 +465,7 @@ func buildProviderName(p providers.Provider, override string) string { | ||||||
| 
 | 
 | ||||||
| // buildRoutesAllowlist builds an []allowedRoute  list from either the legacy
 | // buildRoutesAllowlist builds an []allowedRoute  list from either the legacy
 | ||||||
| // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
 | // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
 | ||||||
| // (method=path support)
 | // (method=path and domain=domain_regex support)
 | ||||||
| func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { | func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { | ||||||
| 	routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) | 	routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) | ||||||
| 
 | 
 | ||||||
|  | @ -492,6 +493,23 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { | ||||||
| 			method = "" | 			method = "" | ||||||
| 			path = parts[0] | 			path = parts[0] | ||||||
| 		} else { | 		} else { | ||||||
|  | 			prefix := strings.ToLower(parts[0]) | ||||||
|  | 			// Check if this is a domain-based route
 | ||||||
|  | 			if prefix == "domain" { | ||||||
|  | 				// Domain-based route: domain=regex
 | ||||||
|  | 				domainPattern := parts[1] | ||||||
|  | 				compiledDomainRegex, err := regexp.Compile(domainPattern) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, err | ||||||
|  | 				} | ||||||
|  | 				logger.Printf("Skipping auth - Domain: %s", domainPattern) | ||||||
|  | 				routes = append(routes, allowedRoute{ | ||||||
|  | 					method:      "", | ||||||
|  | 					domainRegex: compiledDomainRegex, | ||||||
|  | 				}) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			// Method-based route: method=path
 | ||||||
| 			method = strings.ToUpper(parts[0]) | 			method = strings.ToUpper(parts[0]) | ||||||
| 			path = parts[1] | 			path = parts[1] | ||||||
| 		} | 		} | ||||||
|  | @ -580,6 +598,11 @@ func isAllowedMethod(req *http.Request, route allowedRoute) bool { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func isAllowedPath(req *http.Request, route allowedRoute) bool { | func isAllowedPath(req *http.Request, route allowedRoute) bool { | ||||||
|  | 	// If there's no path regex, consider the path as allowed
 | ||||||
|  | 	if route.pathRegex == nil { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	matches := route.pathRegex.MatchString(requestutil.GetRequestPath(req)) | 	matches := route.pathRegex.MatchString(requestutil.GetRequestPath(req)) | ||||||
| 
 | 
 | ||||||
| 	if route.negate { | 	if route.negate { | ||||||
|  | @ -589,10 +612,20 @@ func isAllowedPath(req *http.Request, route allowedRoute) bool { | ||||||
| 	return matches | 	return matches | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // IsAllowedRoute is used to check if the request method & path is allowed without auth
 | func isAllowedDomain(req *http.Request, route allowedRoute) bool { | ||||||
|  | 	// If there's no domain regex, consider the domain as allowed
 | ||||||
|  | 	if route.domainRegex == nil { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	host := requestutil.GetRequestHost(req) | ||||||
|  | 	return route.domainRegex.MatchString(host) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsAllowedRoute is used to check if the request method, path, and domain are 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 isAllowedMethod(req, route) && isAllowedPath(req, route) { | 		if isAllowedMethod(req, route) && isAllowedPath(req, route) && isAllowedDomain(req, route) { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -2252,6 +2252,7 @@ func Test_buildRoutesAllowlist(t *testing.T) { | ||||||
| 		method            string | 		method            string | ||||||
| 		negate            bool | 		negate            bool | ||||||
| 		regexString       string | 		regexString       string | ||||||
|  | 		domainRegexString string | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
|  | @ -2375,6 +2376,49 @@ func Test_buildRoutesAllowlist(t *testing.T) { | ||||||
| 			}, | 			}, | ||||||
| 			shouldError: false, | 			shouldError: false, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Domain-based routes", | ||||||
|  | 			skipAuthRegex: []string{}, | ||||||
|  | 			skipAuthRoutes: []string{ | ||||||
|  | 				"domain=example\\.com", | ||||||
|  | 				"domain=.*\\.subdomain\\.com", | ||||||
|  | 			}, | ||||||
|  | 			expectedRoutes: []expectedAllowedRoute{ | ||||||
|  | 				{ | ||||||
|  | 					method:            "", | ||||||
|  | 					domainRegexString: "example\\.com", | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					method:            "", | ||||||
|  | 					domainRegexString: ".*\\.subdomain\\.com", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			shouldError: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Mixed method and domain routes", | ||||||
|  | 			skipAuthRegex: []string{}, | ||||||
|  | 			skipAuthRoutes: []string{ | ||||||
|  | 				"GET=^/api/v1", | ||||||
|  | 				"domain=api\\.example\\.com", | ||||||
|  | 				"POST=^/webhook", | ||||||
|  | 			}, | ||||||
|  | 			expectedRoutes: []expectedAllowedRoute{ | ||||||
|  | 				{ | ||||||
|  | 					method:      "GET", | ||||||
|  | 					regexString: "^/api/v1", | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					method:            "", | ||||||
|  | 					domainRegexString: "api\\.example\\.com", | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					method:      "POST", | ||||||
|  | 					regexString: "^/webhook", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			shouldError: false, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "Invalid skipAuthRegex entry", | 			name: "Invalid skipAuthRegex entry", | ||||||
| 			skipAuthRegex: []string{ | 			skipAuthRegex: []string{ | ||||||
|  | @ -2417,7 +2461,22 @@ func Test_buildRoutesAllowlist(t *testing.T) { | ||||||
| 				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.negate, tc.expectedRoutes[i].negate) | ||||||
|  | 
 | ||||||
|  | 				// Check path regex if expected
 | ||||||
|  | 				if tc.expectedRoutes[i].regexString != "" { | ||||||
|  | 					assert.NotNil(t, route.pathRegex) | ||||||
| 					assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString) | 					assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString) | ||||||
|  | 				} else { | ||||||
|  | 					assert.Nil(t, route.pathRegex) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check domain regex if expected
 | ||||||
|  | 				if tc.expectedRoutes[i].domainRegexString != "" { | ||||||
|  | 					assert.NotNil(t, route.domainRegex) | ||||||
|  | 					assert.Equal(t, route.domainRegex.String(), tc.expectedRoutes[i].domainRegexString) | ||||||
|  | 				} else { | ||||||
|  | 					assert.Nil(t, route.domainRegex) | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | @ -2635,6 +2694,118 @@ func TestAllowedRequest(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestAllowedRequestWithDomain(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{ | ||||||
|  | 		"domain=api\\.example\\.com", | ||||||
|  | 		"domain=.*\\.subdomain\\.com", | ||||||
|  | 		"GET=^/api/public", | ||||||
|  | 	} | ||||||
|  | 	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 | ||||||
|  | 		host    string | ||||||
|  | 		allowed bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:    "Domain allowed - exact match", | ||||||
|  | 			method:  "GET", | ||||||
|  | 			url:     "/any/path", | ||||||
|  | 			host:    "api.example.com", | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:    "Domain allowed - subdomain match", | ||||||
|  | 			method:  "POST", | ||||||
|  | 			url:     "/any/path", | ||||||
|  | 			host:    "test.subdomain.com", | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:    "Domain allowed - another subdomain", | ||||||
|  | 			method:  "GET", | ||||||
|  | 			url:     "/different/path", | ||||||
|  | 			host:    "app.subdomain.com", | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:    "Domain denied - wrong domain", | ||||||
|  | 			method:  "GET", | ||||||
|  | 			url:     "/any/path", | ||||||
|  | 			host:    "other.example.com", | ||||||
|  | 			allowed: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:    "Domain denied - no host", | ||||||
|  | 			method:  "GET", | ||||||
|  | 			url:     "/any/path", | ||||||
|  | 			host:    "", | ||||||
|  | 			allowed: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:    "Path route allowed regardless of domain", | ||||||
|  | 			method:  "GET", | ||||||
|  | 			url:     "/api/public", | ||||||
|  | 			host:    "different.com", | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:    "Path route with allowed domain", | ||||||
|  | 			method:  "GET", | ||||||
|  | 			url:     "/api/public", | ||||||
|  | 			host:    "api.example.com", | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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) | ||||||
|  | 			if tc.host != "" { | ||||||
|  | 				req.Host = tc.host | ||||||
|  | 			} | ||||||
|  | 			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 TestAllowedRequestWithForwardedUriHeader(t *testing.T) { | func TestAllowedRequestWithForwardedUriHeader(t *testing.T) { | ||||||
| 	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		w.WriteHeader(200) | 		w.WriteHeader(200) | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ func validateAllowlists(o *options.Options) []string { | ||||||
| 	return msgs | 	return msgs | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
 | // validateAuthRoutes validates method=path routes and domain=domain_regex passed with options.SkipAuthRoutes
 | ||||||
| func validateAuthRoutes(o *options.Options) []string { | func validateAuthRoutes(o *options.Options) []string { | ||||||
| 	msgs := []string{} | 	msgs := []string{} | ||||||
| 	for _, route := range o.SkipAuthRoutes { | 	for _, route := range o.SkipAuthRoutes { | ||||||
|  | @ -36,6 +36,7 @@ func validateAuthRoutes(o *options.Options) []string { | ||||||
| 		if len(parts) == 1 { | 		if len(parts) == 1 { | ||||||
| 			regex = parts[0] | 			regex = parts[0] | ||||||
| 		} else { | 		} else { | ||||||
|  | 			// For method or domain-based routes, validate the regex
 | ||||||
| 			regex = parts[1] | 			regex = parts[1] | ||||||
| 		} | 		} | ||||||
| 		_, err := regexp.Compile(regex) | 		_, err := regexp.Compile(regex) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue