mirror of
https://github.com/thomseddon/traefik-forward-auth.git
synced 2026-02-07 14:42:09 +00:00
Compare commits
4 Commits
v2.1.0
...
fix/open-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62cd687924 | ||
|
|
c3b4ba8244 | ||
|
|
b413c60d42 | ||
|
|
e678a33016 |
@@ -1,2 +1,3 @@
|
||||
example
|
||||
.travis.yml
|
||||
.git
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.12-alpine as builder
|
||||
FROM golang:1.13-alpine as builder
|
||||
|
||||
# Setup
|
||||
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.12-alpine as builder
|
||||
FROM golang:1.13-alpine as builder
|
||||
|
||||
# Setup
|
||||
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -81,9 +82,35 @@ func ValidateEmail(email string) bool {
|
||||
return found
|
||||
}
|
||||
|
||||
// ValidateRedirect validates that the given redirect is valid and permitted for
|
||||
// the given request
|
||||
func ValidateRedirect(r *http.Request, redirect string) error {
|
||||
redirectURL, err := url.Parse(redirect)
|
||||
|
||||
if err != nil {
|
||||
return errors.New("Unable to parse redirect")
|
||||
}
|
||||
|
||||
// If we're using an auth domain?
|
||||
if use, base := useAuthDomain(r); use {
|
||||
// If we are using an auth domain, they redirect must share a common
|
||||
// suffix with the requested redirect
|
||||
if !strings.HasSuffix(redirectURL.Host, base) {
|
||||
return errors.New("Redirect host does not match any expected hosts (should match cookie domain when using auth host)")
|
||||
}
|
||||
} else {
|
||||
// If not, we should only ever redirect to the same domain
|
||||
if redirectURL.Host != r.Header.Get("X-Forwarded-Host") {
|
||||
return errors.New("Redirect host does not match request host (must match when not using auth host)")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
// Get the redirect base
|
||||
// Get the request base from forwarded request
|
||||
func redirectBase(r *http.Request) string {
|
||||
proto := r.Header.Get("X-Forwarded-Proto")
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
|
||||
@@ -95,6 +95,96 @@ func TestAuthValidateEmail(t *testing.T) {
|
||||
assert.True(v, "should allow user in whitelist")
|
||||
}
|
||||
|
||||
func TestAuthValidateRedirect(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
|
||||
newRedirectRequest := func(urlStr string) *http.Request {
|
||||
u, err := url.Parse(urlStr)
|
||||
assert.Nil(err)
|
||||
|
||||
r, _ := http.NewRequest("GET", urlStr, nil)
|
||||
r.Header.Add("X-Forwarded-Proto", u.Scheme)
|
||||
r.Header.Add("X-Forwarded-Host", u.Host)
|
||||
r.Header.Add("X-Forwarded-Uri", u.RequestURI())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
errStr := "Redirect host does not match request host (must match when not using auth host)"
|
||||
|
||||
err := ValidateRedirect(
|
||||
newRedirectRequest("http://app.example.com/_oauth?state=123"),
|
||||
"http://app.example.com.bad.com",
|
||||
)
|
||||
if assert.Error(err) {
|
||||
assert.Equal(errStr, err.Error(), "Should not allow redirect to subdomain")
|
||||
}
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://app.example.com/_oauth?state=123"),
|
||||
"http://app.example.combad.com",
|
||||
)
|
||||
if assert.Error(err) {
|
||||
assert.Equal(errStr, err.Error(), "Should not allow redirect to overlapping domain")
|
||||
}
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://app.example.com/_oauth?state=123"),
|
||||
"http://example.com",
|
||||
)
|
||||
if assert.Error(err) {
|
||||
assert.Equal(errStr, err.Error(), "Should not allow redirect from subdomain")
|
||||
}
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://app.example.com/_oauth?state=123"),
|
||||
"http://app.example.com/profile",
|
||||
)
|
||||
assert.Nil(err, "Should allow same domain")
|
||||
|
||||
//
|
||||
// With Auth Host
|
||||
//
|
||||
config.AuthHost = "auth.example.com"
|
||||
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||
errStr = "Redirect host does not match any expected hosts (should match cookie domain when using auth host)"
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://app.example.com/_oauth?state=123"),
|
||||
"http://app.example.com.bad.com",
|
||||
)
|
||||
if assert.Error(err) {
|
||||
assert.Equal(errStr, err.Error(), "Should not allow redirect to subdomain")
|
||||
}
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://app.example.com/_oauth?state=123"),
|
||||
"http://app.example.combad.com",
|
||||
)
|
||||
if assert.Error(err) {
|
||||
assert.Equal(errStr, err.Error(), "Should not allow redirect to overlapping domain")
|
||||
}
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://auth.example.com/_oauth?state=123"),
|
||||
"http://app.example.com/profile",
|
||||
)
|
||||
assert.Nil(err, "Should allow between subdomains when using auth host")
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://auth.example.com/_oauth?state=123"),
|
||||
"http://auth.example.com/profile",
|
||||
)
|
||||
assert.Nil(err, "Should allow same domain when using auth host")
|
||||
|
||||
err = ValidateRedirect(
|
||||
newRedirectRequest("http://auth.example.com/_oauth?state=123"),
|
||||
"http://example.com/profile",
|
||||
)
|
||||
assert.Nil(err, "Should allow from subdomain when using auth host")
|
||||
}
|
||||
|
||||
func TestRedirectUri(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
|
||||
@@ -25,17 +25,17 @@ type Config struct {
|
||||
|
||||
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
|
||||
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file" json:"-"`
|
||||
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" description:"Domain to set auth cookie on, can be set multiple times"`
|
||||
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" env-delim:"," description:"Domain to set auth cookie on, can be set multiple times"`
|
||||
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
|
||||
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
|
||||
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
|
||||
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
|
||||
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
|
||||
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"`
|
||||
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
|
||||
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
|
||||
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
|
||||
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
|
||||
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Only allow given email addresses, can be set multiple times"`
|
||||
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
|
||||
|
||||
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
|
||||
Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""`
|
||||
|
||||
@@ -197,14 +197,27 @@ func TestConfigParseEnvironment(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
os.Setenv("COOKIE_NAME", "env_cookie_name")
|
||||
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
|
||||
os.Setenv("COOKIE_DOMAIN", "test1.com,example.org")
|
||||
os.Setenv("DOMAIN", "test2.com,example.org")
|
||||
os.Setenv("WHITELIST", "test3.com,example.org")
|
||||
|
||||
c, err := NewConfig([]string{})
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
|
||||
assert.Equal("env_client_id", c.Providers.Google.ClientID, "namespace variable should be read from environment")
|
||||
assert.Equal([]CookieDomain{
|
||||
*NewCookieDomain("test1.com"),
|
||||
*NewCookieDomain("example.org"),
|
||||
}, c.CookieDomains, "array variable should be read from environment COOKIE_DOMAIN")
|
||||
assert.Equal(CommaSeparatedList{"test2.com", "example.org"}, c.Domains, "array variable should be read from environment DOMAIN")
|
||||
assert.Equal(CommaSeparatedList{"test3.com", "example.org"}, c.Whitelist, "array variable should be read from environment WHITELIST")
|
||||
|
||||
os.Unsetenv("COOKIE_NAME")
|
||||
os.Unsetenv("PROVIDERS_GOOGLE_CLIENT_ID")
|
||||
os.Unsetenv("COOKIE_DOMAIN")
|
||||
os.Unsetenv("DOMAIN")
|
||||
os.Unsetenv("WHITELIST")
|
||||
}
|
||||
|
||||
func TestConfigParseEnvironmentBackwardsCompatability(t *testing.T) {
|
||||
|
||||
@@ -143,6 +143,16 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
|
||||
// Clear CSRF cookie
|
||||
http.SetCookie(w, ClearCSRFCookie(r))
|
||||
|
||||
// Validate redirect
|
||||
err = ValidateRedirect(r, redirect)
|
||||
if err != nil {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"receieved_redirect": redirect,
|
||||
}).Warnf("Invalid redirect in CSRF. %v", err)
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
@@ -193,7 +203,8 @@ func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *ht
|
||||
func (s *Server) logger(r *http.Request, rule, msg string) *logrus.Entry {
|
||||
// Create logger
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"source_ip": r.Header.Get("X-Forwarded-For"),
|
||||
"source_ip": r.Header.Get("X-Forwarded-For"),
|
||||
"request_host": r.Header.Get("X-Forwarded-Host"),
|
||||
})
|
||||
|
||||
// Log request
|
||||
|
||||
@@ -141,20 +141,20 @@ func TestServerAuthCallback(t *testing.T) {
|
||||
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
|
||||
|
||||
// Should catch invalid csrf cookie
|
||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
|
||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://example.com")
|
||||
c := MakeCSRFCookie(req, "nononononononononononononononono")
|
||||
res, _ = doHttpRequest(req, c)
|
||||
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
|
||||
|
||||
// Should redirect valid request
|
||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
|
||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://example.com")
|
||||
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
|
||||
res, _ = doHttpRequest(req, c)
|
||||
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed")
|
||||
require.Equal(t, 307, res.StatusCode, "valid auth callback should be allowed")
|
||||
|
||||
fwd, _ := res.Location()
|
||||
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
|
||||
assert.Equal("redirect", fwd.Host, "valid request should be redirected to return url")
|
||||
assert.Equal("example.com", fwd.Host, "valid request should be redirected to return url")
|
||||
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user