git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add API route config (#1760)
965fab4Segfault16
CHANGELOG.md+3
@@ -39,6 +39,9 @@ to remain consistent with CLI flags. You should specify `code_challenge_method`
39
39
40- [#1788](https://github.com/oauth2-proxy/oauth2-proxy/pull/1788) Update base docker image to alpine 3.16
40- [#1788](https://github.com/oauth2-proxy/oauth2-proxy/pull/1788) Update base docker image to alpine 3.16
41
41
42+- [#1760](https://github.com/oauth2-proxy/oauth2-proxy/pull/1760) Option to configure API routes
43+
44+
42# V7.3.0
45# V7.3.0
43
46
44## Release Highlights
47## Release Highlights
contrib/oauth2-proxy.cfg.example+5
@@ -69,6 +69,11 @@
69# "^/metrics"
69# "^/metrics"
70# ]
70# ]
71
71
72+## mark paths as API routes to get HTTP Status code 401 instead of redirect to login page
73+# api_routes = [
74+# "^/api
75+# ]
76+
72## Templates
77## Templates
73## optional directory with custom sign_in.html and error.html
78## optional directory with custom sign_in.html and error.html
74# custom_templates_dir = ""
79# custom_templates_dir = ""
docs/docs/configuration/overview.md+1
@@ -75,6 +75,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
75| Option | Type | Description | Default |
75| Option | Type | Description | Default |
76| ------ | ---- | ----------- | ------- |
76| ------ | ---- | ----------- | ------- |
77| `--acr-values` | string | optional, see [docs](https://openid.net/specs/openid-connect-eap-acr-values-1_0.html#acrValues) | `""` |
77| `--acr-values` | string | optional, see [docs](https://openid.net/specs/openid-connect-eap-acr-values-1_0.html#acrValues) | `""` |
78+| `--api-route` | string \| list | return HTTP 401 instead of redirecting to authentication server if token is not valid. Format: path_regex | |
78| `--approval-prompt` | string | OAuth approval_prompt | `"force"` |
79| `--approval-prompt` | string | OAuth approval_prompt | `"force"` |
79| `--auth-logging` | bool | Log authentication attempts | true |
80| `--auth-logging` | bool | Log authentication attempts | true |
80| `--auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) |
81| `--auth-logging-format` | string | Template for authentication log lines | see [Logging Configuration](#logging-configuration) |
oauthproxy.go+39−1
@@ -68,6 +68,10 @@ type allowedRoute struct {
68 pathRegex *regexp.Regexp
68 pathRegex *regexp.Regexp
69}
69}
70
70
71+type apiRoute struct {
72+ pathRegex *regexp.Regexp
73+}
74+
71// OAuthProxy is the main authentication proxy
75// OAuthProxy is the main authentication proxy
72type OAuthProxy struct {
76type OAuthProxy struct {
73 CookieOptions *options.Cookie
77 CookieOptions *options.Cookie
@@ -76,6 +80,7 @@ type OAuthProxy struct {
76 SignInPath string
80 SignInPath string
77
81
78 allowedRoutes []allowedRoute
82 allowedRoutes []allowedRoute
83+ apiRoutes []apiRoute
79 redirectURL *url.URL // the url to receive requests at
84 redirectURL *url.URL // the url to receive requests at
80 whitelistDomains []string
85 whitelistDomains []string
81 provider providers.Provider
86 provider providers.Provider
@@ -176,6 +181,11 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
176 return nil, err
181 return nil, err
177 }
182 }
178
183
184+ apiRoutes, err := buildAPIRoutes(opts)
185+ if err != nil {
186+ return nil, err
187+ }
188+
179 preAuthChain, err := buildPreAuthChain(opts)
189 preAuthChain, err := buildPreAuthChain(opts)
180 if err != nil {
190 if err != nil {
181 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
191 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
@@ -202,6 +212,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
202 provider: provider,
212 provider: provider,
203 sessionStore: sessionStore,
213 sessionStore: sessionStore,
204 redirectURL: redirectURL,
214 redirectURL: redirectURL,
215+ apiRoutes: apiRoutes,
205 allowedRoutes: allowedRoutes,
216 allowedRoutes: allowedRoutes,
206 whitelistDomains: opts.WhitelistDomains,
217 whitelistDomains: opts.WhitelistDomains,
207 skipAuthPreflight: opts.SkipAuthPreflight,
218 skipAuthPreflight: opts.SkipAuthPreflight,
@@ -473,6 +484,24 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
473 return routes, nil
484 return routes, nil
474}
485}
475
486
487+// buildAPIRoutes builds an []apiRoute from ApiRoutes option
488+func buildAPIRoutes(opts *options.Options) ([]apiRoute, error) {
489+ routes := make([]apiRoute, 0, len(opts.APIRoutes))
490+
491+ for _, path := range opts.APIRoutes {
492+ compiledRegex, err := regexp.Compile(path)
493+ if err != nil {
494+ return nil, err
495+ }
496+ logger.Printf("API route - Path: %s", path)
497+ routes = append(routes, apiRoute{
498+ pathRegex: compiledRegex,
499+ })
500+ }
501+
502+ return routes, nil
503+}
504+
476// ClearSessionCookie creates a cookie to unset the user's authentication cookie
505// ClearSessionCookie creates a cookie to unset the user's authentication cookie
477// stored in the user's session
506// stored in the user's session
478func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
507func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
@@ -543,6 +572,15 @@ func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
543 return false
572 return false
544}
573}
545
574
575+func (p *OAuthProxy) isAPIPath(req *http.Request) bool {
576+ for _, route := range p.apiRoutes {
577+ if route.pathRegex.MatchString(req.URL.Path) {
578+ return true
579+ }
580+ }
581+ return false
582+}
583+
546// isTrustedIP is used to check if a request comes from a trusted client IP address.
584// isTrustedIP is used to check if a request comes from a trusted client IP address.
547func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
585func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
548 if p.trustedIPs == nil {
586 if p.trustedIPs == nil {
@@ -911,7 +949,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
911 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
949 p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req)
912 case ErrNeedsLogin:
950 case ErrNeedsLogin:
913 // we need to send the user to a login screen
951 // we need to send the user to a login screen
914− if p.forceJSONErrors || isAjax(req) {
952+ if p.forceJSONErrors || isAjax(req) || p.isAPIPath(req) {
915 logger.Printf("No valid authentication in request. Access Denied.")
953 logger.Printf("No valid authentication in request. Access Denied.")
916 // no point redirecting an AJAX request
954 // no point redirecting an AJAX request
917 p.errorJSON(rw, http.StatusUnauthorized)
955 p.errorJSON(rw, http.StatusUnauthorized)
oauthproxy_test.go+112−1
@@ -1533,7 +1533,8 @@ func (st *SignatureTest) Close() {
1533
1533
1534// fakeNetConn simulates an http.Request.Body buffer that will be consumed
1534// fakeNetConn simulates an http.Request.Body buffer that will be consumed
1535// when it is read by the hmacauth.HmacAuth if not handled properly. See:
1535// when it is read by the hmacauth.HmacAuth if not handled properly. See:
1536+//
1536−// https://github.com/18F/hmacauth/pull/4
1537+// https://github.com/18F/hmacauth/pull/4
1537type fakeNetConn struct {
1538type fakeNetConn struct {
1538 reqBody string
1539 reqBody string
1539}
1540}
@@ -2421,6 +2422,116 @@ func Test_buildRoutesAllowlist(t *testing.T) {
2421 }
2422 }
2422}
2423}
2423
2424
2425+func TestApiRoutes(t *testing.T) {
2426+
2427+ ajaxAPIServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2428+ w.WriteHeader(200)
2429+ _, err := w.Write([]byte("AJAX API Request"))
2430+ if err != nil {
2431+ t.Fatal(err)
2432+ }
2433+ }))
2434+ t.Cleanup(ajaxAPIServer.Close)
2435+
2436+ apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2437+ w.WriteHeader(200)
2438+ _, err := w.Write([]byte("API Request"))
2439+ if err != nil {
2440+ t.Fatal(err)
2441+ }
2442+ }))
2443+ t.Cleanup(apiServer.Close)
2444+
2445+ uiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2446+ w.WriteHeader(200)
2447+ _, err := w.Write([]byte("API Request"))
2448+ if err != nil {
2449+ t.Fatal(err)
2450+ }
2451+ }))
2452+ t.Cleanup(uiServer.Close)
2453+
2454+ opts := baseTestOptions()
2455+ opts.UpstreamServers = options.UpstreamConfig{
2456+ Upstreams: []options.Upstream{
2457+ {
2458+ ID: apiServer.URL,
2459+ Path: "/api",
2460+ URI: apiServer.URL,
2461+ },
2462+ {
2463+ ID: ajaxAPIServer.URL,
2464+ Path: "/ajaxapi",
2465+ URI: ajaxAPIServer.URL,
2466+ },
2467+ {
2468+ ID: uiServer.URL,
2469+ Path: "/ui",
2470+ URI: uiServer.URL,
2471+ },
2472+ },
2473+ }
2474+ opts.APIRoutes = []string{
2475+ "^/api",
2476+ }
2477+ opts.SkipProviderButton = true
2478+ err := validation.Validate(opts)
2479+ assert.NoError(t, err)
2480+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2481+ if err != nil {
2482+ t.Fatal(err)
2483+ }
2484+
2485+ testCases := []struct {
2486+ name string
2487+ contentType string
2488+ url string
2489+ shouldRedirect bool
2490+ }{
2491+ {
2492+ name: "AJAX request matching API regex",
2493+ contentType: "application/json",
2494+ url: "/api/v1/UserInfo",
2495+ shouldRedirect: false,
2496+ },
2497+ {
2498+ name: "AJAX request not matching API regex",
2499+ contentType: "application/json",
2500+ url: "/ajaxapi/v1/UserInfo",
2501+ shouldRedirect: false,
2502+ },
2503+ {
2504+ name: "Other Request matching API regex",
2505+ contentType: "application/grpcwebtext",
2506+ url: "/api/v1/UserInfo",
2507+ shouldRedirect: false,
2508+ },
2509+ {
2510+ name: "UI request",
2511+ contentType: "html",
2512+ url: "/ui/index.html",
2513+ shouldRedirect: true,
2514+ },
2515+ }
2516+
2517+ for _, tc := range testCases {
2518+ t.Run(tc.name, func(t *testing.T) {
2519+ req, err := http.NewRequest("GET", tc.url, nil)
2520+ req.Header.Set("Accept", tc.contentType)
2521+ assert.NoError(t, err)
2522+
2523+ rw := httptest.NewRecorder()
2524+ proxy.ServeHTTP(rw, req)
2525+
2526+ if tc.shouldRedirect {
2527+ assert.Equal(t, 302, rw.Code)
2528+ } else {
2529+ assert.Equal(t, 401, rw.Code)
2530+ }
2531+ })
2532+ }
2533+}
2534+
2424func TestAllowedRequest(t *testing.T) {
2535func TestAllowedRequest(t *testing.T) {
2425 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2536 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2426 w.WriteHeader(200)
2537 w.WriteHeader(200)
pkg/apis/options/options.go+2
@@ -50,6 +50,7 @@ type Options struct {
50
50
51 Providers Providers `cfg:",internal"`
51 Providers Providers `cfg:",internal"`
52
52
53+ APIRoutes []string `flag:"api-route" cfg:"api_routes"`
53 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
54 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
54 SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
55 SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
55 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
56 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
@@ -116,6 +117,7 @@ func NewFlagSet() *pflag.FlagSet {
116 flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
117 flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
117 flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)")
118 flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)")
118 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")
119 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")
120+ flagSet.StringSlice("api-route", []string{}, "return HTTP 401 instead of redirecting to authentication server if token is not valid. Format: path_regex")
119 flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
121 flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
120 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
122 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
121 flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
123 flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
pkg/validation/allowlist.go+23−13
@@ -13,8 +13,8 @@ import (
13func validateAllowlists(o *options.Options) []string {
13func validateAllowlists(o *options.Options) []string {
14 msgs := []string{}
14 msgs := []string{}
15
15
16− msgs = append(msgs, validateRoutes(o)...)
16+ msgs = append(msgs, validateAuthRoutes(o)...)
17− msgs = append(msgs, validateRegexes(o)...)
17+ msgs = append(msgs, validateAuthRegexes(o)...)
18 msgs = append(msgs, validateTrustedIPs(o)...)
18 msgs = append(msgs, validateTrustedIPs(o)...)
19
19
20 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
20 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
@@ -27,8 +27,8 @@ func validateAllowlists(o *options.Options) []string {
27 return msgs
27 return msgs
28}
28}
29
29
30−// validateRoutes validates method=path routes passed with options.SkipAuthRoutes
30+// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
31−func validateRoutes(o *options.Options) []string {
31+func validateAuthRoutes(o *options.Options) []string {
32 msgs := []string{}
32 msgs := []string{}
33 for _, route := range o.SkipAuthRoutes {
33 for _, route := range o.SkipAuthRoutes {
34 var regex string
34 var regex string
@@ -47,15 +47,8 @@ func validateRoutes(o *options.Options) []string {
47}
47}
48
48
49// validateRegex validates regex paths passed with options.SkipAuthRegex
49// validateRegex validates regex paths passed with options.SkipAuthRegex
50−func validateRegexes(o *options.Options) []string {
50+func validateAuthRegexes(o *options.Options) []string {
51− msgs := []string{}
52− for _, regex := range o.SkipAuthRegex {
51+ return validateRegexes(o.SkipAuthRegex)
53− _, err := regexp.Compile(regex)
54− if err != nil {
55− msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err))
56− }
57− }
58− return msgs
59}
52}
60
53
61// validateTrustedIPs validates IP/CIDRs for IP based allowlists
54// validateTrustedIPs validates IP/CIDRs for IP based allowlists
@@ -68,3 +61,20 @@ func validateTrustedIPs(o *options.Options) []string {
68 }
61 }
69 return msgs
62 return msgs
70}
63}
64+
65+// validateAPIRoutes validates regex paths passed with options.ApiRoutes
66+func validateAPIRoutes(o *options.Options) []string {
67+ return validateRegexes(o.APIRoutes)
68+}
69+
70+// validateRegexes validates all regexes and returns a list of messages in case of error
71+func validateRegexes(regexes []string) []string {
72+ msgs := []string{}
73+ for _, regex := range regexes {
74+ _, err := regexp.Compile(regex)
75+ if err != nil {
76+ msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err))
77+ }
78+ }
79+ return msgs
80+}
pkg/validation/allowlist_test.go+2−2
@@ -29,7 +29,7 @@ var _ = Describe("Allowlist", func() {
29 opts := &options.Options{
29 opts := &options.Options{
30 SkipAuthRoutes: r.routes,
30 SkipAuthRoutes: r.routes,
31 }
31 }
32− Expect(validateRoutes(opts)).To(ConsistOf(r.errStrings))
32+ Expect(validateAuthRoutes(opts)).To(ConsistOf(r.errStrings))
33 },
33 },
34 Entry("Valid regex routes", &validateRoutesTableInput{
34 Entry("Valid regex routes", &validateRoutesTableInput{
35 routes: []string{
35 routes: []string{
@@ -61,7 +61,7 @@ var _ = Describe("Allowlist", func() {
61 opts := &options.Options{
61 opts := &options.Options{
62 SkipAuthRegex: r.regexes,
62 SkipAuthRegex: r.regexes,
63 }
63 }
64− Expect(validateRegexes(opts)).To(ConsistOf(r.errStrings))
64+ Expect(validateAuthRegexes(opts)).To(ConsistOf(r.errStrings))
65 },
65 },
66 Entry("Valid regex routes", &validateRegexesTableInput{
66 Entry("Valid regex routes", &validateRegexesTableInput{
67 regexes: []string{
67 regexes: []string{
pkg/validation/options.go+1
@@ -25,6 +25,7 @@ func Validate(o *options.Options) error {
25 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
25 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
26 msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
26 msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
27 msgs = append(msgs, validateProviders(o)...)
27 msgs = append(msgs, validateProviders(o)...)
28+ msgs = append(msgs, validateAPIRoutes(o)...)
28 msgs = configureLogger(o.Logging, msgs)
29 msgs = configureLogger(o.Logging, msgs)
29 msgs = parseSignatureKey(o, msgs)
30 msgs = parseSignatureKey(o, msgs)
30
31