1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-11-24 08:52:25 +02:00

Integrate redirect package with OAuth2 Proxy

This commit is contained in:
Joel Speed 2021-06-05 12:34:31 +01:00
parent e1764d4221
commit 273ab1f591
No known key found for this signature in database
GPG Key ID: 6E80578D6751DEFB
2 changed files with 24 additions and 678 deletions

View File

@ -22,6 +22,7 @@ import (
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
@ -55,10 +56,6 @@ var (
// ErrAccessDenied means the user should receive a 401 Unauthorized response
ErrAccessDenied = errors.New("access denied")
// Used to check final redirects are not susceptible to open redirects.
// Matches //, /\ and both of these with whitespace in between (eg / / or / \).
invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
)
// allowedRoute manages method + path based allowlists
@ -87,13 +84,15 @@ type OAuthProxy struct {
realClientIPParser ipapi.RealClientIPParser
trustedIPs *ip.NetSet
sessionChain alice.Chain
headersChain alice.Chain
preAuthChain alice.Chain
pageWriter pagewriter.Writer
server proxyhttp.Server
upstreamProxy http.Handler
serveMux *mux.Router
sessionChain alice.Chain
headersChain alice.Chain
preAuthChain alice.Chain
pageWriter pagewriter.Writer
server proxyhttp.Server
upstreamProxy http.Handler
serveMux *mux.Router
redirectValidator redirect.Validator
appDirector redirect.AppDirector
}
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@ -176,6 +175,12 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
return nil, fmt.Errorf("could not build headers chain: %v", err)
}
redirectValidator := redirect.NewValidator(opts.WhitelistDomains)
appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{
ProxyPrefix: opts.ProxyPrefix,
Validator: redirectValidator,
})
p := &OAuthProxy{
CookieOptions: &opts.Cookie,
Validator: validator,
@ -200,6 +205,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
preAuthChain: preAuthChain,
pageWriter: pageWriter,
upstreamProxy: upstreamProxy,
redirectValidator: redirectValidator,
appDirector: appDirector,
}
p.buildServeMux(opts.ProxyPrefix)
@ -465,59 +472,13 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *s
return p.sessionStore.Save(rw, req, s)
}
// IsValidRedirect checks whether the redirect URL is whitelisted
func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
switch {
case redirect == "":
// The user didn't specify a redirect, should fallback to `/`
return false
case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
return true
case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
redirectURL, err := url.Parse(redirect)
if err != nil {
logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
return false
}
redirectHostname := redirectURL.Hostname()
for _, allowedDomain := range p.whitelistDomains {
allowedHost, allowedPort := splitHostPort(allowedDomain)
if allowedHost == "" {
continue
}
if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
(strings.HasPrefix(allowedHost, ".") &&
strings.HasSuffix(redirectHostname, allowedHost)) {
// the domain names match, now validate the ports
// if the whitelisted domain's port is '*', allow all ports
// if the whitelisted domain contains a specific port, only allow that port
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
redirectPort := redirectURL.Port()
if allowedPort == "*" ||
allowedPort == redirectPort ||
(allowedPort == "" && redirectPort == "") {
return true
}
}
}
logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
return false
default:
logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
return false
}
}
func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
p.serveMux.ServeHTTP(rw, req)
}
// ErrorPage writes an error response
func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) {
redirectURL, err := p.getAppRedirect(req)
redirectURL, err := p.appDirector.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
}
@ -582,7 +543,7 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
}
rw.WriteHeader(code)
redirectURL, err := p.getAppRedirect(req)
redirectURL, err := p.appDirector.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
@ -617,7 +578,7 @@ func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
// SignIn serves a page prompting users to sign in
func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
redirect, err := p.getAppRedirect(req)
redirect, err := p.appDirector.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
@ -681,7 +642,7 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
// SignOut sends a response to clear the authentication cookie
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
redirect, err := p.getAppRedirect(req)
redirect, err := p.appDirector.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
@ -707,7 +668,7 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
return
}
appRedirect, err := p.getAppRedirect(req)
appRedirect, err := p.appDirector.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining application redirect: %v", err)
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
@ -790,7 +751,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
csrf.SetSessionNonce(session)
p.provider.ValidateSession(req.Context(), session)
if !p.IsValidRedirect(appRedirect) {
if !p.redirectValidator.IsValidRedirect(appRedirect) {
appRedirect = "/"
}
@ -947,158 +908,6 @@ func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
return rd.String()
}
// getAppRedirect 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 (p *OAuthProxy) getAppRedirect(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.
type redirectGetter func(req *http.Request) string
for _, rdGetter := range []redirectGetter{
p.getRdQuerystringRedirect,
p.getXAuthRequestRedirect,
p.getXForwardedHeadersRedirect,
p.getURIRedirect,
} {
redirect := rdGetter(req)
// Call `p.IsValidRedirect` again here a final time to be safe
if redirect != "" && p.IsValidRedirect(redirect) {
return redirect, nil
}
}
return "/", nil
}
func isForwardedRequest(req *http.Request) bool {
return requestutil.IsProxied(req) &&
req.Host != requestutil.GetRequestHost(req)
}
func (p *OAuthProxy) hasProxyPrefix(path string) bool {
return strings.HasPrefix(path, fmt.Sprintf("%s/", p.ProxyPrefix))
}
func (p *OAuthProxy) validateRedirect(redirect string, errorFormat string) string {
if p.IsValidRedirect(redirect) {
return redirect
}
if redirect != "" {
logger.Errorf(errorFormat, redirect)
}
return ""
}
// getRdQuerystringRedirect handles this getAppRedirect strategy:
// - `rd` querysting parameter
func (p *OAuthProxy) getRdQuerystringRedirect(req *http.Request) string {
return p.validateRedirect(
req.Form.Get("rd"),
"Invalid redirect provided in rd querystring parameter: %s",
)
}
// getXAuthRequestRedirect handles this getAppRedirect strategy:
// - `X-Auth-Request-Redirect` Header
func (p *OAuthProxy) getXAuthRequestRedirect(req *http.Request) string {
return p.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 (p *OAuthProxy) getXForwardedHeadersRedirect(req *http.Request) string {
if !isForwardedRequest(req) {
return ""
}
uri := requestutil.GetRequestURI(req)
if p.hasProxyPrefix(uri) {
uri = "/"
}
redirect := fmt.Sprintf(
"%s://%s%s",
requestutil.GetRequestProto(req),
requestutil.GetRequestHost(req),
uri,
)
return p.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 (p *OAuthProxy) getURIRedirect(req *http.Request) string {
redirect := p.validateRedirect(
requestutil.GetRequestURI(req),
"Invalid redirect generated from X-Forwarded-Uri header: %s",
)
if redirect == "" {
redirect = req.URL.RequestURI()
}
if p.hasProxyPrefix(redirect) {
return "/"
}
return redirect
}
// splitHostPort separates host and port. If the port is not valid, it returns
// the entire input as host, and it doesn't check the validity of the host.
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
// *** taken from net/url, modified validOptionalPort() to accept ":*"
func splitHostPort(hostport string) (host, port string) {
host = hostport
colon := strings.LastIndexByte(host, ':')
if colon != -1 && validOptionalPort(host[colon:]) {
host, port = host[:colon], host[colon+1:]
}
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = host[1 : len(host)-1]
}
return
}
// validOptionalPort reports whether port is either an empty string
// or matches /^:\d*$/
// *** taken from net/url, modified to accept ":*"
func validOptionalPort(port string) bool {
if port == "" || port == ":*" {
return true
}
if port[0] != ':' {
return false
}
for _, b := range port[1:] {
if b < '0' || b > '9' {
return false
}
}
return true
}
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
// Returns:
// - `nil, ErrNeedsLogin` if user needs to login.

View File

@ -1,7 +1,6 @@
package main
import (
"bufio"
"context"
"crypto"
"encoding/base64"
@ -11,7 +10,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"regexp"
"strings"
"testing"
@ -19,7 +17,6 @@ import (
"github.com/coreos/go-oidc"
"github.com/mbland/hmacauth"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
@ -28,10 +25,7 @@ import (
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
"github.com/oauth2-proxy/oauth2-proxy/v7/providers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
@ -63,304 +57,6 @@ func TestRobotsTxt(t *testing.T) {
assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String())
}
func TestIsValidRedirect(t *testing.T) {
opts := baseTestOptions()
// Should match domains that are exactly foo.bar and any subdomain of bar.foo
opts.WhitelistDomains = []string{
"foo.bar",
".bar.foo",
"port.bar:8080",
".sub.port.bar:8080",
"anyport.bar:*",
".sub.anyport.bar:*",
}
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 {
Desc, Redirect string
ExpectedResult bool
}{
{
Desc: "noRD",
Redirect: "",
ExpectedResult: false,
},
{
Desc: "singleSlash",
Redirect: "/redirect",
ExpectedResult: true,
},
{
Desc: "doubleSlash",
Redirect: "//redirect",
ExpectedResult: false,
},
{
Desc: "validHTTP",
Redirect: "http://foo.bar/redirect",
ExpectedResult: true,
},
{
Desc: "validHTTPS",
Redirect: "https://foo.bar/redirect",
ExpectedResult: true,
},
{
Desc: "invalidHTTPSubdomain",
Redirect: "http://baz.foo.bar/redirect",
ExpectedResult: false,
},
{
Desc: "invalidHTTPSSubdomain",
Redirect: "https://baz.foo.bar/redirect",
ExpectedResult: false,
},
{
Desc: "validHTTPSubdomain",
Redirect: "http://baz.bar.foo/redirect",
ExpectedResult: true,
},
{
Desc: "validHTTPSSubdomain",
Redirect: "https://baz.bar.foo/redirect",
ExpectedResult: true,
},
{
Desc: "validHTTPDomain",
Redirect: "http://bar.foo/redirect",
ExpectedResult: true,
},
{
Desc: "invalidHTTP1",
Redirect: "http://foo.bar.evil.corp/redirect",
ExpectedResult: false,
},
{
Desc: "invalidHTTPS1",
Redirect: "https://foo.bar.evil.corp/redirect",
ExpectedResult: false,
},
{
Desc: "invalidHTTP2",
Redirect: "http://evil.corp/redirect?rd=foo.bar",
ExpectedResult: false,
},
{
Desc: "invalidHTTPS2",
Redirect: "https://evil.corp/redirect?rd=foo.bar",
ExpectedResult: false,
},
{
Desc: "invalidPort",
Redirect: "https://evil.corp:3838/redirect",
ExpectedResult: false,
},
{
Desc: "invalidEmptyPort",
Redirect: "http://foo.bar:3838/redirect",
ExpectedResult: false,
},
{
Desc: "invalidEmptyPortSubdomain",
Redirect: "http://baz.bar.foo:3838/redirect",
ExpectedResult: false,
},
{
Desc: "validSpecificPort",
Redirect: "http://port.bar:8080/redirect",
ExpectedResult: true,
},
{
Desc: "invalidSpecificPort",
Redirect: "http://port.bar:3838/redirect",
ExpectedResult: false,
},
{
Desc: "validSpecificPortSubdomain",
Redirect: "http://foo.sub.port.bar:8080/redirect",
ExpectedResult: true,
},
{
Desc: "invalidSpecificPortSubdomain",
Redirect: "http://foo.sub.port.bar:3838/redirect",
ExpectedResult: false,
},
{
Desc: "validAnyPort1",
Redirect: "http://anyport.bar:8080/redirect",
ExpectedResult: true,
},
{
Desc: "validAnyPort2",
Redirect: "http://anyport.bar:8081/redirect",
ExpectedResult: true,
},
{
Desc: "validAnyPortSubdomain1",
Redirect: "http://a.sub.anyport.bar:8080/redirect",
ExpectedResult: true,
},
{
Desc: "validAnyPortSubdomain2",
Redirect: "http://a.sub.anyport.bar:8081/redirect",
ExpectedResult: true,
},
{
Desc: "openRedirect1",
Redirect: "/\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectSpace1",
Redirect: "/ /evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectSpace2",
Redirect: "/ \\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectTab1",
Redirect: "/\t/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectTab2",
Redirect: "/\t\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectVerticalTab1",
Redirect: "/\v/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectVerticalTab2",
Redirect: "/\v\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectNewLine1",
Redirect: "/\n/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectNewLine2",
Redirect: "/\n\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectCarriageReturn1",
Redirect: "/\r/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectCarriageReturn2",
Redirect: "/\r\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectTripleTab",
Redirect: "/\t\t/\t/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectTripleTab2",
Redirect: "/\t\t\\\t/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectQuadTab1",
Redirect: "/\t\t/\t\t\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectQuadTab2",
Redirect: "/\t\t\\\t\t/evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectPeriod1",
Redirect: "/./\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectPeriod2",
Redirect: "/./../../\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectDoubleTab",
Redirect: "/\t/\t\\evil.com",
ExpectedResult: false,
},
{
Desc: "openRedirectPartialSubdomain",
Redirect: "http://evilbar.foo",
ExpectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.Desc, func(t *testing.T) {
result := proxy.IsValidRedirect(tc.Redirect)
if result != tc.ExpectedResult {
t.Errorf("expected %t got %t", tc.ExpectedResult, result)
}
})
}
}
var _ = Describe("OpenRedirect Tests", func() {
var proxy *OAuthProxy
BeforeEach(func() {
opts := baseTestOptions()
// Should match domains that are exactly foo.bar and any subdomain of bar.foo
opts.WhitelistDomains = []string{
"foo.bar",
".bar.foo",
"port.bar:8080",
".sub.port.bar:8080",
"anyport.bar:*",
".sub.anyport.bar:*",
"www.whitelisteddomain.tld",
}
Expect(validation.Validate(opts)).To(Succeed())
var err error
proxy, err = NewOAuthProxy(opts, func(string) bool { return true })
Expect(err).ToNot(HaveOccurred())
})
file, err := os.Open("./testdata/openredirects.txt")
Expect(err).ToNot(HaveOccurred())
defer func() {
Expect(file.Close()).To(Succeed())
}()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
rd := scanner.Text()
It(rd, func() {
rdUnescaped, err := url.QueryUnescape(rd)
Expect(err).ToNot(HaveOccurred())
Expect(proxy.IsValidRedirect(rdUnescaped)).To(BeFalse(), "Expected redirect not to be valid")
})
}
Expect(scanner.Err()).ToNot(HaveOccurred())
})
type TestProvider struct {
*providers.ProviderData
EmailAddress string
@ -1770,165 +1466,6 @@ func TestRequestSignature(t *testing.T) {
}
}
func Test_getAppRedirect(t *testing.T) {
opts := baseTestOptions()
opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com", ".example.com:8443")
err := validation.Validate(opts)
assert.NoError(t, err)
require.NotEmpty(t, opts.ProxyPrefix)
proxy, err := NewOAuthProxy(opts, func(s string) bool { return false })
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
url string
headers map[string]string
reverseProxy bool
expectedRedirect string
}{
{
name: "request outside of ProxyPrefix redirects to original URL",
url: "/foo/bar",
headers: nil,
reverseProxy: false,
expectedRedirect: "/foo/bar",
},
{
name: "request with query preserves query",
url: "/foo?bar",
headers: nil,
reverseProxy: false,
expectedRedirect: "/foo?bar",
},
{
name: "request under ProxyPrefix redirects to root",
url: proxy.ProxyPrefix + "/foo/bar",
headers: nil,
reverseProxy: false,
expectedRedirect: "/",
},
{
name: "proxied request outside of ProxyPrefix redirects to proxied URL",
url: "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,
expectedRedirect: "https://a-service.example.com/foo/bar",
},
{
name: "non-proxied request with spoofed proxy headers wouldn't redirect",
url: "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,
expectedRedirect: "/foo?bar",
},
{
name: "proxied request under ProxyPrefix redirects to root",
url: "https://oauth.example.com" + proxy.ProxyPrefix + "/foo/bar",
headers: map[string]string{
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "a-service.example.com",
"X-Forwarded-Uri": proxy.ProxyPrefix + "/foo/bar",
},
reverseProxy: true,
expectedRedirect: "https://a-service.example.com/",
},
{
name: "proxied request with port under ProxyPrefix redirects to root",
url: "https://oauth.example.com" + proxy.ProxyPrefix + "/foo/bar",
headers: map[string]string{
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "a-service.example.com:8443",
"X-Forwarded-Uri": proxy.ProxyPrefix + "/foo/bar",
},
reverseProxy: true,
expectedRedirect: "https://a-service.example.com:8443/",
},
{
name: "proxied request with missing uri header would still redirect to desired redirect",
url: "https://oauth.example.com/foo?bar",
headers: map[string]string{
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "a-service.example.com",
},
reverseProxy: true,
expectedRedirect: "https://a-service.example.com/foo?bar",
},
{
name: "request with headers proxy not being set (and reverse proxy enabled) would still redirect to desired redirect",
url: "https://oauth.example.com/foo?bar",
headers: nil,
reverseProxy: true,
expectedRedirect: "/foo?bar",
},
{
name: "proxied request with X-Auth-Request-Redirect being set outside of ProxyPrefix redirects to proxied URL",
url: "https://oauth.example.com/foo/bar",
headers: map[string]string{
"X-Auth-Request-Redirect": "https://a-service.example.com/foo/bar",
},
reverseProxy: true,
expectedRedirect: "https://a-service.example.com/foo/bar",
},
{
name: "proxied request with rd query string redirects to proxied URL",
url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbar",
headers: nil,
reverseProxy: false,
expectedRedirect: "https://a-service.example.com/foo/bar",
},
{
name: "proxied request with rd query string and all headers set (and reverse proxy not enabled) redirects to proxied URL on rd query string",
url: "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,
expectedRedirect: "https://a-service.example.com/foo/jazz",
},
{
name: "proxied request with rd query string and some headers set redirects to proxied URL on rd query string",
url: "https://oauth.example.com/foo/bar?rd=https%3A%2F%2Fa%2Dservice%2Eexample%2Ecom%2Ffoo%2Fbaz",
headers: map[string]string{
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "another-service.example.com",
"X-Forwarded-Uri": "/seasons/greetings",
},
reverseProxy: true,
expectedRedirect: "https://a-service.example.com/foo/baz",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", tt.url, nil)
for header, value := range tt.headers {
if value != "" {
req.Header.Add(header, value)
}
}
req = middleware.AddRequestScope(req, &middleware.RequestScope{
ReverseProxy: tt.reverseProxy,
})
redirect, err := proxy.getAppRedirect(req)
assert.NoError(t, err)
assert.Equal(t, tt.expectedRedirect, redirect)
})
}
}
type ajaxRequestTest struct {
opts *options.Options
proxy *OAuthProxy