mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-03-23 21:50:48 +02:00
Create AppDirector for getting the application redirect URL
This commit is contained in:
parent
e7f304fc96
commit
e1764d4221
pkg
96
pkg/app/redirect/director.go
Normal file
96
pkg/app/redirect/director.go
Normal file
@ -0,0 +1,96 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||
)
|
||||
|
||||
// AppDirector is responsible for determining where OAuth2 Proxy should redirect
|
||||
// a users request to after the user has authenticated with the identity provider.
|
||||
type AppDirector interface {
|
||||
GetRedirect(req *http.Request) (string, error)
|
||||
}
|
||||
|
||||
// AppDirectorOpts are the requirements for constructing a new AppDirector.
|
||||
type AppDirectorOpts struct {
|
||||
ProxyPrefix string
|
||||
Validator Validator
|
||||
}
|
||||
|
||||
// NewAppDirector constructs a new AppDirector for getting the application
|
||||
// redirect URL.
|
||||
func NewAppDirector(opts AppDirectorOpts) AppDirector {
|
||||
prefix := opts.ProxyPrefix
|
||||
if !strings.HasSuffix(prefix, "/") {
|
||||
prefix = fmt.Sprintf("%s/", prefix)
|
||||
}
|
||||
|
||||
return &appDirector{
|
||||
proxyPrefix: prefix,
|
||||
validator: opts.Validator,
|
||||
}
|
||||
}
|
||||
|
||||
// appDirector implements the AppDirector interface.
|
||||
type appDirector struct {
|
||||
proxyPrefix string
|
||||
validator Validator
|
||||
}
|
||||
|
||||
// GetRedirect determines the full URL or URI path to redirect clients to once
|
||||
// authenticated with the OAuthProxy.
|
||||
// Strategy priority (first legal result is used):
|
||||
// - `rd` querysting parameter
|
||||
// - `X-Auth-Request-Redirect` header
|
||||
// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
|
||||
// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
|
||||
// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
|
||||
// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
|
||||
// - `/`
|
||||
func (a *appDirector) GetRedirect(req *http.Request) (string, error) {
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// These redirect getter functions are strategies ordered by priority
|
||||
// for figuring out the redirect URL.
|
||||
for _, rdGetter := range []redirectGetter{
|
||||
a.getRdQuerystringRedirect,
|
||||
a.getXAuthRequestRedirect,
|
||||
a.getXForwardedHeadersRedirect,
|
||||
a.getURIRedirect,
|
||||
} {
|
||||
redirect := rdGetter(req)
|
||||
// Call `p.IsValidRedirect` again here a final time to be safe
|
||||
if redirect != "" && a.validator.IsValidRedirect(redirect) {
|
||||
return redirect, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "/", nil
|
||||
}
|
||||
|
||||
// validateRedirect checks that the redirect is valid.
|
||||
// When an invalid, non-empty redirect is found, an error will be logged using
|
||||
// the provided format.
|
||||
func (a *appDirector) validateRedirect(redirect string, errorFormat string) string {
|
||||
if a.validator.IsValidRedirect(redirect) {
|
||||
return redirect
|
||||
}
|
||||
if redirect != "" {
|
||||
logger.Errorf(errorFormat, redirect)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// hasProxyPrefix determines whether the obtained path would be a request to
|
||||
// one of OAuth2 Proxy's own endpoints, eg. th callback URL.
|
||||
// Redirects to these endpoints should not be allowed as they will create
|
||||
// redirection loops.
|
||||
func (a *appDirector) hasProxyPrefix(path string) bool {
|
||||
return strings.HasPrefix(path, a.proxyPrefix)
|
||||
}
|
177
pkg/app/redirect/director_test.go
Normal file
177
pkg/app/redirect/director_test.go
Normal file
@ -0,0 +1,177 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const testProxyPrefix = "/oauth2"
|
||||
|
||||
var _ = Describe("Director Suite", func() {
|
||||
type getRedirectTableInput struct {
|
||||
requestURL string
|
||||
headers map[string]string
|
||||
reverseProxy bool
|
||||
validator Validator
|
||||
expectedRedirect string
|
||||
}
|
||||
|
||||
DescribeTable("GetRedirect",
|
||||
func(in getRedirectTableInput) {
|
||||
appDirector := NewAppDirector(AppDirectorOpts{
|
||||
ProxyPrefix: testProxyPrefix,
|
||||
Validator: in.validator,
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", in.requestURL, nil)
|
||||
for header, value := range in.headers {
|
||||
if value != "" {
|
||||
req.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
||||
ReverseProxy: in.reverseProxy,
|
||||
})
|
||||
|
||||
redirect, err := appDirector.GetRedirect(req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(redirect).To(Equal(in.expectedRedirect))
|
||||
},
|
||||
Entry("Request outside of the proxy prefix, redirects to original request", getRedirectTableInput{
|
||||
requestURL: "/foo/bar",
|
||||
headers: nil,
|
||||
reverseProxy: false,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "/foo/bar",
|
||||
}),
|
||||
Entry("Request with query, preserves the query", getRedirectTableInput{
|
||||
requestURL: "/foo?bar",
|
||||
headers: nil,
|
||||
reverseProxy: false,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "/foo?bar",
|
||||
}),
|
||||
Entry("Request under the proxy prefix, redirects to root", getRedirectTableInput{
|
||||
requestURL: testProxyPrefix + "/foo/bar",
|
||||
headers: nil,
|
||||
reverseProxy: false,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "/",
|
||||
}),
|
||||
Entry("Proxied request with headers, outside of ProxyPrefix, redirects to proxied URL", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo/bar",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "a-service.example.com",
|
||||
"X-Forwarded-Uri": "/foo/bar",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/foo/bar",
|
||||
}),
|
||||
Entry("Non-proxied request with spoofed headers, wouldn't redirect", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo?bar",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "a-service.example.com",
|
||||
"X-Forwarded-Uri": "/foo/bar",
|
||||
},
|
||||
reverseProxy: false,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "/foo?bar",
|
||||
}),
|
||||
Entry("Proxied request with headers, under ProxyPrefix, redirects to root", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com" + testProxyPrefix + "/foo/bar",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "a-service.example.com",
|
||||
"X-Forwarded-Uri": testProxyPrefix + "/foo/bar",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/",
|
||||
}),
|
||||
Entry("Proxied request with port, under ProxyPrefix, redirects to root", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com" + testProxyPrefix + "/foo/bar",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "a-service.example.com:8443",
|
||||
"X-Forwarded-Uri": testProxyPrefix + "/foo/bar",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com:8443/",
|
||||
}),
|
||||
Entry("Proxied request with headers, missing URI header, redirects to the desired redirect URL", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo?bar",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "a-service.example.com",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/foo?bar",
|
||||
}),
|
||||
Entry("Proxied request without headers, with reverse proxy enabled, redirects to the desired URL", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo?bar",
|
||||
headers: nil,
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "/foo?bar",
|
||||
}),
|
||||
Entry("Proxied request with X-Auth-Request-Redirect, outside of ProxyPrefix, redirects to proxied URL", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo/bar",
|
||||
headers: map[string]string{
|
||||
"X-Auth-Request-Redirect": "https://a-service.example.com/foo/bar",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/foo/bar",
|
||||
}),
|
||||
Entry("Proxied request with RD parameter, outside of ProxyPrefix, redirects to proxied URL", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbar",
|
||||
headers: nil,
|
||||
reverseProxy: false,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/foo/bar",
|
||||
}),
|
||||
Entry("Proxied request with RD parameter and all headers set, reverse proxy disabled, redirects to proxied URL based on the RD parameter", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
|
||||
headers: map[string]string{
|
||||
"X-Auth-Request-Redirect": "https://a-service.example.com/foo/baz",
|
||||
"X-Forwarded-Proto": "http",
|
||||
"X-Forwarded-Host": "another-service.example.com",
|
||||
"X-Forwarded-Uri": "/seasons/greetings",
|
||||
},
|
||||
reverseProxy: false,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/foo/jazz",
|
||||
}),
|
||||
Entry("Proxied request with RD parameter and some headers set, reverse proxy enabled, redirects to proxied URL based on the RD parameter", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "http",
|
||||
"X-Forwarded-Host": "another-service.example.com",
|
||||
"X-Forwarded-Uri": "/seasons/greetings",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(true),
|
||||
expectedRedirect: "https://a-service.example.com/foo/jazz",
|
||||
}),
|
||||
Entry("Proxied request with invalid RD parameter and some headers set, reverse proxy enabled, redirects to proxied URL based on the headers", getRedirectTableInput{
|
||||
requestURL: "https://oauth.example.com/foo/bar?rd=http%3A%2F%2Fanother%2Dservice%2Eexample%2Ecom%2Ffoo%2Fjazz",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "a-service.example.com",
|
||||
"X-Forwarded-Uri": "/foo/bar",
|
||||
},
|
||||
reverseProxy: true,
|
||||
validator: testValidator(false, "https://a-service.example.com/foo/bar"),
|
||||
expectedRedirect: "https://a-service.example.com/foo/bar",
|
||||
}),
|
||||
)
|
||||
})
|
73
pkg/app/redirect/getters.go
Normal file
73
pkg/app/redirect/getters.go
Normal file
@ -0,0 +1,73 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
|
||||
)
|
||||
|
||||
// redirectGetter represents a method to allow the proxy to determine a redirect
|
||||
// based on the original request.
|
||||
type redirectGetter func(req *http.Request) string
|
||||
|
||||
// getRdQuerystringRedirect handles this getAppRedirect strategy:
|
||||
// - `rd` querysting parameter
|
||||
func (a *appDirector) getRdQuerystringRedirect(req *http.Request) string {
|
||||
return a.validateRedirect(
|
||||
req.Form.Get("rd"),
|
||||
"Invalid redirect provided in rd querystring parameter: %s",
|
||||
)
|
||||
}
|
||||
|
||||
// getXAuthRequestRedirect handles this getAppRedirect strategy:
|
||||
// - `X-Auth-Request-Redirect` Header
|
||||
func (a *appDirector) getXAuthRequestRedirect(req *http.Request) string {
|
||||
return a.validateRedirect(
|
||||
req.Header.Get("X-Auth-Request-Redirect"),
|
||||
"Invalid redirect provided in X-Auth-Request-Redirect header: %s",
|
||||
)
|
||||
}
|
||||
|
||||
// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
|
||||
// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
|
||||
// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
|
||||
func (a *appDirector) getXForwardedHeadersRedirect(req *http.Request) string {
|
||||
if !requestutil.IsForwardedRequest(req) {
|
||||
return ""
|
||||
}
|
||||
|
||||
uri := requestutil.GetRequestURI(req)
|
||||
if a.hasProxyPrefix(uri) {
|
||||
uri = "/"
|
||||
}
|
||||
|
||||
redirect := fmt.Sprintf(
|
||||
"%s://%s%s",
|
||||
requestutil.GetRequestProto(req),
|
||||
requestutil.GetRequestHost(req),
|
||||
uri,
|
||||
)
|
||||
|
||||
return a.validateRedirect(redirect,
|
||||
"Invalid redirect generated from X-Forwarded-* headers: %s")
|
||||
}
|
||||
|
||||
// getURIRedirect handles these getAppRedirect strategies:
|
||||
// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
|
||||
// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
|
||||
// - `/`
|
||||
func (a *appDirector) getURIRedirect(req *http.Request) string {
|
||||
redirect := a.validateRedirect(
|
||||
requestutil.GetRequestURI(req),
|
||||
"Invalid redirect generated from X-Forwarded-Uri header: %s",
|
||||
)
|
||||
if redirect == "" {
|
||||
redirect = req.URL.RequestURI()
|
||||
}
|
||||
|
||||
if a.hasProxyPrefix(redirect) {
|
||||
return "/"
|
||||
}
|
||||
return redirect
|
||||
}
|
@ -15,3 +15,25 @@ func TestOptionsSuite(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Redirect Suite")
|
||||
}
|
||||
|
||||
// testValidator creates a mock validator that will always return the given result.
|
||||
func testValidator(result bool, allowedRedirects ...string) Validator {
|
||||
return &mockValidator{result: result, allowedRedirects: allowedRedirects}
|
||||
}
|
||||
|
||||
// mockValidator implements the Validator interface for use in testing.
|
||||
type mockValidator struct {
|
||||
result bool
|
||||
allowedRedirects []string
|
||||
}
|
||||
|
||||
// IsValidRedirect implements the Validator interface.
|
||||
func (m *mockValidator) IsValidRedirect(redirect string) bool {
|
||||
for _, allowed := range m.allowedRedirects {
|
||||
if redirect == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return m.result
|
||||
}
|
||||
|
@ -52,3 +52,8 @@ func IsProxied(req *http.Request) bool {
|
||||
}
|
||||
return scope.ReverseProxy
|
||||
}
|
||||
|
||||
func IsForwardedRequest(req *http.Request) bool {
|
||||
return IsProxied(req) &&
|
||||
req.Host != GetRequestHost(req)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user