1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-02-15 14:03:45 +02:00

Use X-Forwarded-{Proto,Host,Uri} on redirect as last resort (#957)

This commit is contained in:
İlteriş Eroğlu 2021-01-02 02:23:11 +03:00 committed by GitHub
parent 91b3f5973e
commit 1d74a51cd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 263 additions and 2 deletions

View File

@ -6,6 +6,7 @@
- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Keycloak will now use `--profile-url` if set for the userinfo endpoint
instead of `--validate-url`. `--validate-url` will still work for backwards compatibility.
- [#957](https://github.com/oauth2-proxy/oauth2-proxy/pull/957) To use X-Forwarded-{Proto,Host,Uri} on redirect detection, `--reverse-proxy` must be `true`.
- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) `--user-id-claim` option is deprecated and replaced by `--oidc-email-claim`
- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) `/oauth2/auth` `allowed_groups` querystring parameter can be paired with the `allowed-groups` configuration option.
@ -59,6 +60,7 @@
## Changes since v6.1.1
- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Migrate Keycloak to EnrichSession & support multiple groups for authorization (@NickMeves)
- [#957](https://github.com/oauth2-proxy/oauth2-proxy/pull/957) Use X-Forwarded-{Proto,Host,Uri} on redirect as last resort (@linuxgemini)
- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Add support for Gitlab project based authentication (@factorysh)
- [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed)
- [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves)

View File

@ -106,7 +106,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
| `--request-logging` | bool | Log requests | true |
| `--request-logging-format` | string | Template for request log lines | see [Logging Configuration](#logging-configuration) |
| `--resource` | string | The resource that is protected (Azure AD only) | |
| `--reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted | false |
| `--reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted and allows X-Forwarded-{Proto,Host,Uri} headers to be used on redirect selection | false |
| `--scope` | string | OAuth scope specification | |
| `--session-cookie-minimal` | bool | strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only) | false |
| `--session-store-type` | string | [Session data storage backend](sessions.md); redis or cookie | cookie |
@ -354,6 +354,73 @@ It is recommended to use `--session-store-type=redis` when expecting large sessi
You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=".
## Configuring for use with the Traefik (v2) `ForwardAuth` middleware
**This option requires `--reverse-proxy` option to be set.**
The [Traefik v2 `ForwardAuth` middleware](https://doc.traefik.io/traefik/middlewares/forwardauth/) allows Traefik to authenticate requests via the oauth2-proxy's `/oauth2/auth` endpoint on every request, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the whole request through. For example, on Dynamic File (YAML) Configuration:
```yaml
http:
routers:
a-service:
rule: "Host(`a-service.example.com`)"
service: a-service-backend
middlewares:
- oauth-errors
- oauth-auth
tls:
certResolver: default
domains:
- main: "example.com"
sans:
- "*.example.com"
oauth:
rule: "Host(`a-service.example.com`, `oauth.example.com`) && PathPrefix(`/oauth2/`)"
middlewares:
- auth-headers
service: oauth-backend
tls:
certResolver: default
domains:
- main: "example.com"
sans:
- "*.example.com"
services:
a-service-backend:
loadBalancer:
servers:
- url: http://172.16.0.2:7555
oauth-backend:
loadBalancer:
servers:
- url: http://172.16.0.1:4180
middlewares:
auth-headers:
headers:
sslRedirect: true
stsSeconds: 315360000
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
sslHost: example.com
stsIncludeSubdomains: true
stsPreload: true
frameDeny: true
oauth-auth:
forwardAuth:
address: https://oauth.example.com/oauth2/auth
trustForwardHeader: true
oauth-errors:
errors:
status:
- "401-403"
service: oauth-backend
query: "/oauth2/sign_in"
```
:::note
If you set up your OAuth2 provider to rotate your client secret, you can use the `client-secret-file` option to reload the secret when it is updated.
:::

View File

@ -98,6 +98,7 @@ type OAuthProxy struct {
SetAuthorization bool
PassAuthorization bool
PreferEmailToUser bool
ReverseProxy bool
skipAuthPreflight bool
skipJwtBearerTokens bool
templates *template.Template
@ -200,6 +201,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
ProxyPrefix: opts.ProxyPrefix,
ReverseProxy: opts.ReverseProxy,
provider: opts.GetProvider(),
providerNameOverride: opts.ProviderName,
sessionStore: sessionStore,
@ -578,10 +580,18 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error)
if req.Form.Get("rd") != "" {
redirect = req.Form.Get("rd")
}
// Quirk: On reverse proxies that doesn't have support for
// "X-Auth-Request-Redirect" header or dynamic header/query string
// manipulation (like Traefik v1 and v2), we can try if the header
// X-Forwarded-Host exists or not.
if redirect == "" && isForwardedRequest(req, p.ReverseProxy) {
redirect = p.getRedirectFromForwardHeaders(req)
}
if !p.IsValidRedirect(redirect) {
// Use RequestURI to preserve ?query
redirect = req.URL.RequestURI()
if strings.HasPrefix(redirect, p.ProxyPrefix) {
if strings.HasPrefix(redirect, fmt.Sprintf("%s/", p.ProxyPrefix)) {
redirect = "/"
}
}
@ -589,6 +599,17 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error)
return
}
// getRedirectFromForwardHeaders returns the redirect URL based on X-Forwarded-{Proto,Host,Uri} headers
func (p *OAuthProxy) getRedirectFromForwardHeaders(req *http.Request) string {
uri := util.GetRequestURI(req)
if strings.HasPrefix(uri, fmt.Sprintf("%s/", p.ProxyPrefix)) {
uri = "/"
}
return fmt.Sprintf("%s://%s%s", util.GetRequestProto(req), util.GetRequestHost(req), uri)
}
// 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.
@ -686,6 +707,12 @@ func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
return false
}
// isForwardedRequest is used to check if X-Forwarded-Host header exists or not
func isForwardedRequest(req *http.Request, reverseProxy bool) bool {
isForwarded := req.Host != util.GetRequestHost(req)
return isForwarded && reverseProxy
}
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
var noCacheHeaders = map[string]string{
"Expires": time.Unix(0, 0).Format(time.RFC1123),

View File

@ -1750,6 +1750,8 @@ func TestRequestSignature(t *testing.T) {
func TestGetRedirect(t *testing.T) {
opts := baseTestOptions()
opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com")
opts.WhitelistDomains = append(opts.WhitelistDomains, ".example.com:8443")
err := validation.Validate(opts)
assert.NoError(t, err)
require.NotEmpty(t, opts.ProxyPrefix)
@ -1761,27 +1763,145 @@ func TestGetRedirect(t *testing.T) {
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",
"X-Forwarded-Proto": "",
"X-Forwarded-Host": "",
"X-Forwarded-Uri": "",
},
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-Auth-Request-Redirect": "",
"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)
}
}
proxy.ReverseProxy = tt.reverseProxy
redirect, err := proxy.GetRedirect(req)
assert.NoError(t, err)

View File

@ -25,6 +25,15 @@ func GetCertPool(paths []string) (*x509.CertPool, error) {
return pool, nil
}
// GetRequestProto return the request host header or X-Forwarded-Proto if present
func GetRequestProto(req *http.Request) string {
proto := req.Header.Get("X-Forwarded-Proto")
if proto == "" {
proto = req.URL.Scheme
}
return proto
}
// GetRequestHost return the request host header or X-Forwarded-Host if present
func GetRequestHost(req *http.Request) string {
host := req.Header.Get("X-Forwarded-Host")
@ -33,3 +42,13 @@ func GetRequestHost(req *http.Request) string {
}
return host
}
// GetRequestURI return the request host header or X-Forwarded-Uri if present
func GetRequestURI(req *http.Request) string {
uri := req.Header.Get("X-Forwarded-Uri")
if uri == "" {
// Use RequestURI to preserve ?query
uri = req.URL.RequestURI()
}
return uri
}

View File

@ -110,3 +110,29 @@ func TestGetRequestHost(t *testing.T) {
extHost := GetRequestHost(proxyReq)
g.Expect(extHost).To(Equal("external.example.com"))
}
func TestGetRequestProto(t *testing.T) {
g := NewWithT(t)
req := httptest.NewRequest("GET", "https://example.com", nil)
proto := GetRequestProto(req)
g.Expect(proto).To(Equal("https"))
proxyReq := httptest.NewRequest("GET", "https://internal.example.com", nil)
proxyReq.Header.Add("X-Forwarded-Proto", "http")
extProto := GetRequestProto(proxyReq)
g.Expect(extProto).To(Equal("http"))
}
func TestGetRequestURI(t *testing.T) {
g := NewWithT(t)
req := httptest.NewRequest("GET", "https://example.com/ping", nil)
uri := GetRequestURI(req)
g.Expect(uri).To(Equal("/ping"))
proxyReq := httptest.NewRequest("GET", "http://internal.example.com/bong", nil)
proxyReq.Header.Add("X-Forwarded-Uri", "/ping")
extURI := GetRequestURI(proxyReq)
g.Expect(extURI).To(Equal("/ping"))
}