git.iko.soy/typeable/oauth2-proxy
on master help sign in
Create AppDirector for getting the application redirect URL
commite1764d42215fd99ee675661bbaf7b22f256b8f8e authorJoel Speed <joel.speed@hotmail.co.uk> dateSun 30 May 2021, 09:34 (5 years ago) parente7f304f
5 files changed+373
pkg/app/redirect/director.go+96 pkg/app/redirect/director_test.go+177 pkg/app/redirect/getters.go+73 pkg/app/redirect/pagewriter_suite_test.go+22 pkg/requests/util/util.go+5
pkg/app/redirect/director.gonew file+96
@@ -0,0 +1,96 @@
1+package redirect
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "strings"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
9+)
10+
11+// AppDirector is responsible for determining where OAuth2 Proxy should redirect
12+// a users request to after the user has authenticated with the identity provider.
13+type AppDirector interface {
14+ GetRedirect(req *http.Request) (string, error)
15+}
16+
17+// AppDirectorOpts are the requirements for constructing a new AppDirector.
18+type AppDirectorOpts struct {
19+ ProxyPrefix string
20+ Validator Validator
21+}
22+
23+// NewAppDirector constructs a new AppDirector for getting the application
24+// redirect URL.
25+func NewAppDirector(opts AppDirectorOpts) AppDirector {
26+ prefix := opts.ProxyPrefix
27+ if !strings.HasSuffix(prefix, "/") {
28+ prefix = fmt.Sprintf("%s/", prefix)
29+ }
30+
31+ return &appDirector{
32+ proxyPrefix: prefix,
33+ validator: opts.Validator,
34+ }
35+}
36+
37+// appDirector implements the AppDirector interface.
38+type appDirector struct {
39+ proxyPrefix string
40+ validator Validator
41+}
42+
43+// GetRedirect determines the full URL or URI path to redirect clients to once
44+// authenticated with the OAuthProxy.
45+// Strategy priority (first legal result is used):
46+// - `rd` querysting parameter
47+// - `X-Auth-Request-Redirect` header
48+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
49+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
50+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
51+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
52+// - `/`
53+func (a *appDirector) GetRedirect(req *http.Request) (string, error) {
54+ err := req.ParseForm()
55+ if err != nil {
56+ return "", err
57+ }
58+
59+ // These redirect getter functions are strategies ordered by priority
60+ // for figuring out the redirect URL.
61+ for _, rdGetter := range []redirectGetter{
62+ a.getRdQuerystringRedirect,
63+ a.getXAuthRequestRedirect,
64+ a.getXForwardedHeadersRedirect,
65+ a.getURIRedirect,
66+ } {
67+ redirect := rdGetter(req)
68+ // Call `p.IsValidRedirect` again here a final time to be safe
69+ if redirect != "" && a.validator.IsValidRedirect(redirect) {
70+ return redirect, nil
71+ }
72+ }
73+
74+ return "/", nil
75+}
76+
77+// validateRedirect checks that the redirect is valid.
78+// When an invalid, non-empty redirect is found, an error will be logged using
79+// the provided format.
80+func (a *appDirector) validateRedirect(redirect string, errorFormat string) string {
81+ if a.validator.IsValidRedirect(redirect) {
82+ return redirect
83+ }
84+ if redirect != "" {
85+ logger.Errorf(errorFormat, redirect)
86+ }
87+ return ""
88+}
89+
90+// hasProxyPrefix determines whether the obtained path would be a request to
91+// one of OAuth2 Proxy's own endpoints, eg. th callback URL.
92+// Redirects to these endpoints should not be allowed as they will create
93+// redirection loops.
94+func (a *appDirector) hasProxyPrefix(path string) bool {
95+ return strings.HasPrefix(path, a.proxyPrefix)
96+}
pkg/app/redirect/director_test.gonew file+177
@@ -0,0 +1,177 @@
1+package redirect
2+
3+import (
4+ "net/http"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/ginkgo/extensions/table"
9+ . "github.com/onsi/gomega"
10+)
11+
12+const testProxyPrefix = "/oauth2"
13+
14+var _ = Describe("Director Suite", func() {
15+ type getRedirectTableInput struct {
16+ requestURL string
17+ headers map[string]string
18+ reverseProxy bool
19+ validator Validator
20+ expectedRedirect string
21+ }
22+
23+ DescribeTable("GetRedirect",
24+ func(in getRedirectTableInput) {
25+ appDirector := NewAppDirector(AppDirectorOpts{
26+ ProxyPrefix: testProxyPrefix,
27+ Validator: in.validator,
28+ })
29+
30+ req, _ := http.NewRequest("GET", in.requestURL, nil)
31+ for header, value := range in.headers {
32+ if value != "" {
33+ req.Header.Add(header, value)
34+ }
35+ }
36+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
37+ ReverseProxy: in.reverseProxy,
38+ })
39+
40+ redirect, err := appDirector.GetRedirect(req)
41+ Expect(err).ToNot(HaveOccurred())
42+ Expect(redirect).To(Equal(in.expectedRedirect))
43+ },
44+ Entry("Request outside of the proxy prefix, redirects to original request", getRedirectTableInput{
45+ requestURL: "/foo/bar",
46+ headers: nil,
47+ reverseProxy: false,
48+ validator: testValidator(true),
49+ expectedRedirect: "/foo/bar",
50+ }),
51+ Entry("Request with query, preserves the query", getRedirectTableInput{
52+ requestURL: "/foo?bar",
53+ headers: nil,
54+ reverseProxy: false,
55+ validator: testValidator(true),
56+ expectedRedirect: "/foo?bar",
57+ }),
58+ Entry("Request under the proxy prefix, redirects to root", getRedirectTableInput{
59+ requestURL: testProxyPrefix + "/foo/bar",
60+ headers: nil,
61+ reverseProxy: false,
62+ validator: testValidator(true),
63+ expectedRedirect: "/",
64+ }),
65+ Entry("Proxied request with headers, outside of ProxyPrefix, redirects to proxied URL", getRedirectTableInput{
66+ requestURL: "https://oauth.example.com/foo/bar",
67+ headers: map[string]string{
68+ "X-Forwarded-Proto": "https",
69+ "X-Forwarded-Host": "a-service.example.com",
70+ "X-Forwarded-Uri": "/foo/bar",
71+ },
72+ reverseProxy: true,
73+ validator: testValidator(true),
74+ expectedRedirect: "https://a-service.example.com/foo/bar",
75+ }),
76+ Entry("Non-proxied request with spoofed headers, wouldn't redirect", getRedirectTableInput{
77+ requestURL: "https://oauth.example.com/foo?bar",
78+ headers: map[string]string{
79+ "X-Forwarded-Proto": "https",
80+ "X-Forwarded-Host": "a-service.example.com",
81+ "X-Forwarded-Uri": "/foo/bar",
82+ },
83+ reverseProxy: false,
84+ validator: testValidator(true),
85+ expectedRedirect: "/foo?bar",
86+ }),
87+ Entry("Proxied request with headers, under ProxyPrefix, redirects to root", getRedirectTableInput{
88+ requestURL: "https://oauth.example.com" + testProxyPrefix + "/foo/bar",
89+ headers: map[string]string{
90+ "X-Forwarded-Proto": "https",
91+ "X-Forwarded-Host": "a-service.example.com",
92+ "X-Forwarded-Uri": testProxyPrefix + "/foo/bar",
93+ },
94+ reverseProxy: true,
95+ validator: testValidator(true),
96+ expectedRedirect: "https://a-service.example.com/",
97+ }),
98+ Entry("Proxied request with port, under ProxyPrefix, redirects to root", getRedirectTableInput{
99+ requestURL: "https://oauth.example.com" + testProxyPrefix + "/foo/bar",
100+ headers: map[string]string{
101+ "X-Forwarded-Proto": "https",
102+ "X-Forwarded-Host": "a-service.example.com:8443",
103+ "X-Forwarded-Uri": testProxyPrefix + "/foo/bar",
104+ },
105+ reverseProxy: true,
106+ validator: testValidator(true),
107+ expectedRedirect: "https://a-service.example.com:8443/",
108+ }),
109+ Entry("Proxied request with headers, missing URI header, redirects to the desired redirect URL", getRedirectTableInput{
110+ requestURL: "https://oauth.example.com/foo?bar",
111+ headers: map[string]string{
112+ "X-Forwarded-Proto": "https",
113+ "X-Forwarded-Host": "a-service.example.com",
114+ },
115+ reverseProxy: true,
116+ validator: testValidator(true),
117+ expectedRedirect: "https://a-service.example.com/foo?bar",
118+ }),
119+ Entry("Proxied request without headers, with reverse proxy enabled, redirects to the desired URL", getRedirectTableInput{
120+ requestURL: "https://oauth.example.com/foo?bar",
121+ headers: nil,
122+ reverseProxy: true,
123+ validator: testValidator(true),
124+ expectedRedirect: "/foo?bar",
125+ }),
126+ Entry("Proxied request with X-Auth-Request-Redirect, outside of ProxyPrefix, redirects to proxied URL", getRedirectTableInput{
127+ requestURL: "https://oauth.example.com/foo/bar",
128+ headers: map[string]string{
129+ "X-Auth-Request-Redirect": "https://a-service.example.com/foo/bar",
130+ },
131+ reverseProxy: true,
132+ validator: testValidator(true),
133+ expectedRedirect: "https://a-service.example.com/foo/bar",
134+ }),
135+ Entry("Proxied request with RD parameter, outside of ProxyPrefix, redirects to proxied URL", getRedirectTableInput{
136+ requestURL: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbar",
137+ headers: nil,
138+ reverseProxy: false,
139+ validator: testValidator(true),
140+ expectedRedirect: "https://a-service.example.com/foo/bar",
141+ }),
142+ Entry("Proxied request with RD parameter and all headers set, reverse proxy disabled, redirects to proxied URL based on the RD parameter", getRedirectTableInput{
143+ requestURL: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
144+ headers: map[string]string{
145+ "X-Auth-Request-Redirect": "https://a-service.example.com/foo/baz",
146+ "X-Forwarded-Proto": "http",
147+ "X-Forwarded-Host": "another-service.example.com",
148+ "X-Forwarded-Uri": "/seasons/greetings",
149+ },
150+ reverseProxy: false,
151+ validator: testValidator(true),
152+ expectedRedirect: "https://a-service.example.com/foo/jazz",
153+ }),
154+ Entry("Proxied request with RD parameter and some headers set, reverse proxy enabled, redirects to proxied URL based on the RD parameter", getRedirectTableInput{
155+ requestURL: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
156+ headers: map[string]string{
157+ "X-Forwarded-Proto": "http",
158+ "X-Forwarded-Host": "another-service.example.com",
159+ "X-Forwarded-Uri": "/seasons/greetings",
160+ },
161+ reverseProxy: true,
162+ validator: testValidator(true),
163+ expectedRedirect: "https://a-service.example.com/foo/jazz",
164+ }),
165+ Entry("Proxied request with invalid RD parameter and some headers set, reverse proxy enabled, redirects to proxied URL based on the headers", getRedirectTableInput{
166+ requestURL: "https://oauth.example.com/foo/bar?rd=http%3A%2F%2Fanother%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
167+ headers: map[string]string{
168+ "X-Forwarded-Proto": "https",
169+ "X-Forwarded-Host": "a-service.example.com",
170+ "X-Forwarded-Uri": "/foo/bar",
171+ },
172+ reverseProxy: true,
173+ validator: testValidator(false, "https://a-service.example.com/foo/bar"),
174+ expectedRedirect: "https://a-service.example.com/foo/bar",
175+ }),
176+ )
177+})
pkg/app/redirect/getters.gonew file+73
@@ -0,0 +1,73 @@
1+package redirect
2+
3+import (
4+ "fmt"
5+ "net/http"
6+
7+ requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
8+)
9+
10+// redirectGetter represents a method to allow the proxy to determine a redirect
11+// based on the original request.
12+type redirectGetter func(req *http.Request) string
13+
14+// getRdQuerystringRedirect handles this getAppRedirect strategy:
15+// - `rd` querysting parameter
16+func (a *appDirector) getRdQuerystringRedirect(req *http.Request) string {
17+ return a.validateRedirect(
18+ req.Form.Get("rd"),
19+ "Invalid redirect provided in rd querystring parameter: %s",
20+ )
21+}
22+
23+// getXAuthRequestRedirect handles this getAppRedirect strategy:
24+// - `X-Auth-Request-Redirect` Header
25+func (a *appDirector) getXAuthRequestRedirect(req *http.Request) string {
26+ return a.validateRedirect(
27+ req.Header.Get("X-Auth-Request-Redirect"),
28+ "Invalid redirect provided in X-Auth-Request-Redirect header: %s",
29+ )
30+}
31+
32+// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
33+// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
34+// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
35+func (a *appDirector) getXForwardedHeadersRedirect(req *http.Request) string {
36+ if !requestutil.IsForwardedRequest(req) {
37+ return ""
38+ }
39+
40+ uri := requestutil.GetRequestURI(req)
41+ if a.hasProxyPrefix(uri) {
42+ uri = "/"
43+ }
44+
45+ redirect := fmt.Sprintf(
46+ "%s://%s%s",
47+ requestutil.GetRequestProto(req),
48+ requestutil.GetRequestHost(req),
49+ uri,
50+ )
51+
52+ return a.validateRedirect(redirect,
53+ "Invalid redirect generated from X-Forwarded-* headers: %s")
54+}
55+
56+// getURIRedirect handles these getAppRedirect strategies:
57+// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
58+// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
59+// - `/`
60+func (a *appDirector) getURIRedirect(req *http.Request) string {
61+ redirect := a.validateRedirect(
62+ requestutil.GetRequestURI(req),
63+ "Invalid redirect generated from X-Forwarded-Uri header: %s",
64+ )
65+ if redirect == "" {
66+ redirect = req.URL.RequestURI()
67+ }
68+
69+ if a.hasProxyPrefix(redirect) {
70+ return "/"
71+ }
72+ return redirect
73+}
pkg/app/redirect/pagewriter_suite_test.go+22
@@ -15,3 +15,25 @@ func TestOptionsSuite(t *testing.T) {
1515 RegisterFailHandler(Fail)
1616 RunSpecs(t, "Redirect Suite")
1717}
18+
19+// testValidator creates a mock validator that will always return the given result.
20+func testValidator(result bool, allowedRedirects ...string) Validator {
21+ return &mockValidator{result: result, allowedRedirects: allowedRedirects}
22+}
23+
24+// mockValidator implements the Validator interface for use in testing.
25+type mockValidator struct {
26+ result bool
27+ allowedRedirects []string
28+}
29+
30+// IsValidRedirect implements the Validator interface.
31+func (m *mockValidator) IsValidRedirect(redirect string) bool {
32+ for _, allowed := range m.allowedRedirects {
33+ if redirect == allowed {
34+ return true
35+ }
36+ }
37+
38+ return m.result
39+}
pkg/requests/util/util.go+5
@@ -52,3 +52,8 @@ func IsProxied(req *http.Request) bool {
5252 }
5353 return scope.ReverseProxy
5454}
55+
56+func IsForwardedRequest(req *http.Request) bool {
57+ return IsProxied(req) &&
58+ req.Host != GetRequestHost(req)
59+}