5 Commits

9 changed files with 185 additions and 28 deletions

View File

@ -98,7 +98,7 @@ Click "Create Credentials" > "OAuth client ID". Select "Web Application", fill i
#### Upgrade Guide #### Upgrade Guide
v2 was released in April 2019, whilst this is fully backwards compatibile, a number of configuration options were modified, please see the [upgrade guide](https://github.com/thomseddon/traefik-forward-auth/wiki/v2-Upgrade-Guide) to prevent warnings on startup and ensure you are using the current configuration. v2 was released in April 2019, whilst this is fully backwards compatible, a number of configuration options were modified, please see the [upgrade guide](https://github.com/thomseddon/traefik-forward-auth/wiki/v2-Upgrade-Guide) to prevent warnings on startup and ensure you are using the current configuration.
## Configuration ## Configuration

1
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/sirupsen/logrus v1.4.1 github.com/sirupsen/logrus v1.4.1
github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486
github.com/vulcand/predicate v1.1.0 // indirect github.com/vulcand/predicate v1.1.0 // indirect
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd // indirect golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd // indirect
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 // indirect golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 // indirect

6
go.sum
View File

@ -55,6 +55,12 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/thomseddon/go-flags v1.4.0 h1:cHj56pbnQxlGo2lx2P8f0Dph4TRYKBJzoPuF2lqNvW4=
github.com/thomseddon/go-flags v1.4.0/go.mod h1:NK9eZpNBmSKVxvyB/MExg6jW0Bo9hQyAuCP+b8MJFow=
github.com/thomseddon/go-flags v1.4.1-0.20190507181358-ce437f05b7fb h1:L311/fJ7WXmFDDtuhf22PkVJqZpqLbEsmGSTEGv7ZQY=
github.com/thomseddon/go-flags v1.4.1-0.20190507181358-ce437f05b7fb/go.mod h1:NK9eZpNBmSKVxvyB/MExg6jW0Bo9hQyAuCP+b8MJFow=
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486 h1:hk17f4niAl4e6viTj2uf/fpfACa6QPmrtMDAo+1tifE=
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486/go.mod h1:NK9eZpNBmSKVxvyB/MExg6jW0Bo9hQyAuCP+b8MJFow=
github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg= github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg=
github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg= github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

View File

@ -14,7 +14,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jessevdk/go-flags" "github.com/thomseddon/go-flags"
"github.com/thomseddon/traefik-forward-auth/internal/provider" "github.com/thomseddon/traefik-forward-auth/internal/provider"
) )
@ -100,7 +100,7 @@ func NewConfig(args []string) (Config, error) {
// Backwards compatability // Backwards compatability
if c.CookieSecretLegacy != "" && c.SecretString == "" { if c.CookieSecretLegacy != "" && c.SecretString == "" {
log.Warn("cookie-secret config option is deprecated, please use secret") fmt.Println("cookie-secret config option is deprecated, please use secret")
c.SecretString = c.CookieSecretLegacy c.SecretString = c.CookieSecretLegacy
} }
if c.ClientIdLegacy != "" { if c.ClientIdLegacy != "" {
@ -110,11 +110,11 @@ func NewConfig(args []string) (Config, error) {
c.Providers.Google.ClientSecret = c.ClientSecretLegacy c.Providers.Google.ClientSecret = c.ClientSecretLegacy
} }
if c.PromptLegacy != "" { if c.PromptLegacy != "" {
log.Warn("prompt config option is deprecated, please use providers.google.prompt") fmt.Println("prompt config option is deprecated, please use providers.google.prompt")
c.Providers.Google.Prompt = c.PromptLegacy c.Providers.Google.Prompt = c.PromptLegacy
} }
if c.CookieSecureLegacy != "" { if c.CookieSecureLegacy != "" {
log.Warn("cookie-secure config option is deprecated, please use insecure-cookie") fmt.Println("cookie-secure config option is deprecated, please use insecure-cookie")
secure, err := strconv.ParseBool(c.CookieSecureLegacy) secure, err := strconv.ParseBool(c.CookieSecureLegacy)
if err != nil { if err != nil {
return c, err return c, err
@ -122,11 +122,11 @@ func NewConfig(args []string) (Config, error) {
c.InsecureCookie = !secure c.InsecureCookie = !secure
} }
if len(c.CookieDomainsLegacy) > 0 { if len(c.CookieDomainsLegacy) > 0 {
log.Warn("cookie-domains config option is deprecated, please use cookie-domain") fmt.Println("cookie-domains config option is deprecated, please use cookie-domain")
c.CookieDomains = append(c.CookieDomains, c.CookieDomainsLegacy...) c.CookieDomains = append(c.CookieDomains, c.CookieDomainsLegacy...)
} }
if len(c.DomainsLegacy) > 0 { if len(c.DomainsLegacy) > 0 {
log.Warn("domains config option is deprecated, please use domain") fmt.Println("domains config option is deprecated, please use domain")
c.Domains = append(c.Domains, c.DomainsLegacy...) c.Domains = append(c.Domains, c.DomainsLegacy...)
} }
@ -141,7 +141,7 @@ func NewConfig(args []string) (Config, error) {
} }
func (c *Config) parseFlags(args []string) error { func (c *Config) parseFlags(args []string) error {
p := flags.NewParser(c, flags.Default) p := flags.NewParser(c, flags.Default|flags.IniUnknownOptionHandler)
p.UnknownOptionHandler = c.parseUnknownFlag p.UnknownOptionHandler = c.parseUnknownFlag
i := flags.NewIniParser(p) i := flags.NewIniParser(p)
@ -157,6 +157,7 @@ func (c *Config) parseFlags(args []string) error {
return err return err
} }
fmt.Println("config format deprecated, please use ini format")
return i.Parse(converted) return i.Parse(converted)
} }
@ -276,6 +277,12 @@ func NewRule() *Rule {
} }
} }
func (r *Rule) formattedRule() string {
// Traefik implements their own "Host" matcher and then offers "HostRegexp"
// to invoke the mux "Host" matcher. This ensures the mux version is used
return strings.ReplaceAll(r.Rule, "Host(", "HostRegexp(")
}
func (r *Rule) Validate() { func (r *Rule) Validate() {
if r.Action != "auth" && r.Action != "allow" { if r.Action != "auth" && r.Action != "allow" {
log.Fatal("invalid rule action, must be \"auth\" or \"allow\"") log.Fatal("invalid rule action, must be \"auth\" or \"allow\"")

View File

@ -165,6 +165,18 @@ func TestConfigParseIni(t *testing.T) {
assert.Equal("inicookiename", c.CookieName, "should be read from ini file") assert.Equal("inicookiename", c.CookieName, "should be read from ini file")
assert.Equal("csrfcookiename", c.CSRFCookieName, "should be read from ini file") assert.Equal("csrfcookiename", c.CSRFCookieName, "should be read from ini file")
assert.Equal("/two", c.Path, "variable in second ini file should override first ini file") assert.Equal("/two", c.Path, "variable in second ini file should override first ini file")
assert.Equal(map[string]*Rule{
"1": {
Action: "allow",
Rule: "PathPrefix(`/one`)",
Provider: "google",
},
"two": {
Action: "auth",
Rule: "Host(`two.com`) && Path(`/two`)",
Provider: "google",
},
}, c.Rules)
} }
func TestConfigFileBackwardsCompatability(t *testing.T) { func TestConfigFileBackwardsCompatability(t *testing.T) {

View File

@ -28,9 +28,9 @@ func (s *Server) buildRoutes() {
// Let's build a router // Let's build a router
for name, rule := range config.Rules { for name, rule := range config.Rules {
if rule.Action == "allow" { if rule.Action == "allow" {
s.router.AddRoute(rule.Rule, 1, s.AllowHandler(name)) s.router.AddRoute(rule.formattedRule(), 1, s.AllowHandler(name))
} else { } else {
s.router.AddRoute(rule.Rule, 1, s.AuthHandler(name)) s.router.AddRoute(rule.formattedRule(), 1, s.AuthHandler(name))
} }
} }
@ -47,6 +47,8 @@ func (s *Server) buildRoutes() {
func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
// Modify request // Modify request
r.Method = r.Header.Get("X-Forwarded-Method")
r.Host = r.Header.Get("X-Forwarded-Host")
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri")) r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
// Pass to mux // Pass to mux

View File

@ -32,7 +32,7 @@ func TestServerAuthHandler(t *testing.T) {
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
// Should redirect vanilla request to login url // Should redirect vanilla request to login url
req := newHttpRequest("/foo") req := newDefaultHttpRequest("/foo")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "vanilla request should be redirected") assert.Equal(307, res.StatusCode, "vanilla request should be redirected")
@ -42,7 +42,7 @@ func TestServerAuthHandler(t *testing.T) {
assert.Equal("/o/oauth2/auth", fwd.Path, "vanilla request should be redirected to google") assert.Equal("/o/oauth2/auth", fwd.Path, "vanilla request should be redirected to google")
// Should catch invalid cookie // Should catch invalid cookie
req = newHttpRequest("/foo") req = newDefaultHttpRequest("/foo")
c := MakeCookie(req, "test@example.com") c := MakeCookie(req, "test@example.com")
parts := strings.Split(c.Value, "|") parts := strings.Split(c.Value, "|")
c.Value = fmt.Sprintf("bad|%s|%s", parts[1], parts[2]) c.Value = fmt.Sprintf("bad|%s|%s", parts[1], parts[2])
@ -51,7 +51,7 @@ func TestServerAuthHandler(t *testing.T) {
assert.Equal(401, res.StatusCode, "invalid cookie should not be authorised") assert.Equal(401, res.StatusCode, "invalid cookie should not be authorised")
// Should validate email // Should validate email
req = newHttpRequest("/foo") req = newDefaultHttpRequest("/foo")
c = MakeCookie(req, "test@example.com") c = MakeCookie(req, "test@example.com")
config.Domains = []string{"test.com"} config.Domains = []string{"test.com"}
@ -59,7 +59,7 @@ func TestServerAuthHandler(t *testing.T) {
assert.Equal(401, res.StatusCode, "invalid email should not be authorised") assert.Equal(401, res.StatusCode, "invalid email should not be authorised")
// Should allow valid request email // Should allow valid request email
req = newHttpRequest("/foo") req = newDefaultHttpRequest("/foo")
c = MakeCookie(req, "test@example.com") c = MakeCookie(req, "test@example.com")
config.Domains = []string{} config.Domains = []string{}
@ -91,18 +91,18 @@ func TestServerAuthCallback(t *testing.T) {
config.Providers.Google.UserURL = userUrl config.Providers.Google.UserURL = userUrl
// Should pass auth response request to callback // Should pass auth response request to callback
req := newHttpRequest("/_oauth") req := newDefaultHttpRequest("/_oauth")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised") assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
// Should catch invalid csrf cookie // Should catch invalid csrf cookie
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect") req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
c := MakeCSRFCookie(req, "nononononononononononononononono") c := MakeCSRFCookie(req, "nononononononononononononononono")
res, _ = doHttpRequest(req, c) res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised") assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
// Should redirect valid request // Should redirect valid request
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect") req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012") c = MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ = doHttpRequest(req, c) res, _ = doHttpRequest(req, c)
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed") assert.Equal(307, res.StatusCode, "valid auth callback should be allowed")
@ -117,33 +117,151 @@ func TestServerDefaultAction(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
req := newHttpRequest("/random") req := newDefaultHttpRequest("/random")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request should require auth with auth default handler") assert.Equal(307, res.StatusCode, "request should require auth with auth default handler")
config.DefaultAction = "allow" config.DefaultAction = "allow"
req = newHttpRequest("/random") req = newDefaultHttpRequest("/random")
res, _ = doHttpRequest(req, nil) res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request should be allowed with default handler") assert.Equal(200, res.StatusCode, "request should be allowed with default handler")
} }
func TestServerRoutePathPrefix(t *testing.T) { func TestServerRouteHeaders(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{ config.Rules = map[string]*Rule{
"web1": { "1": {
Action: "allow", Action: "allow",
Rule: "PathPrefix(`/api`)", Rule: "Headers(`X-Test`, `test123`)",
},
"2": {
Action: "allow",
Rule: "HeadersRegexp(`X-Test`, `test(456|789)`)",
}, },
} }
// Should block any request // Should block any request
req := newHttpRequest("/random") req := newDefaultHttpRequest("/random")
req.Header.Add("X-Random", "hello")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching
req = newDefaultHttpRequest("/api")
req.Header.Add("X-Test", "test123")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
// Should allow matching
req = newDefaultHttpRequest("/api")
req.Header.Add("X-Test", "test789")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
}
func TestServerRouteHost(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{
"1": {
Action: "allow",
Rule: "Host(`api.example.com`)",
},
"2": {
Action: "allow",
Rule: "HostRegexp(`sub{num:[0-9]}.example.com`)",
},
}
// Should block any request
req := newHttpRequest("GET", "https://example.com/", "/")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request
req = newHttpRequest("GET", "https://api.example.com/", "/")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
// Should allow matching request
req = newHttpRequest("GET", "https://sub8.example.com/", "/")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
}
func TestServerRouteMethod(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{
"1": {
Action: "allow",
Rule: "Method(`PUT`)",
},
}
// Should block any request
req := newHttpRequest("GET", "https://example.com/", "/")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request
req = newHttpRequest("PUT", "https://example.com/", "/")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
}
func TestServerRoutePath(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{
"1": {
Action: "allow",
Rule: "Path(`/api`)",
},
"2": {
Action: "allow",
Rule: "PathPrefix(`/private`)",
},
}
// Should block any request
req := newDefaultHttpRequest("/random")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth") assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow /api request // Should allow /api request
req = newHttpRequest("/api") req = newDefaultHttpRequest("/api")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
// Should allow /private request
req = newDefaultHttpRequest("/private")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
req = newDefaultHttpRequest("/private/path")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
}
func TestServerRouteQuery(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{
"1": {
Action: "allow",
Rule: "Query(`q=test123`)",
},
}
// Should block any request
req := newHttpRequest("GET", "https://example.com/", "/?q=no")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request
req = newHttpRequest("GET", "https://api.example.com/", "/?q=test123")
res, _ = doHttpRequest(req, nil) res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed") assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
} }
@ -194,8 +312,15 @@ func doHttpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) {
return res, string(body) return res, string(body)
} }
func newHttpRequest(uri string) *http.Request { func newDefaultHttpRequest(uri string) *http.Request {
r := httptest.NewRequest("", "http://example.com/", nil) return newHttpRequest("", "http://example.com/", uri)
}
func newHttpRequest(method, dest, uri string) *http.Request {
r := httptest.NewRequest("", "http://should-use-x-forwarded.com", nil)
p, _ := url.Parse(dest)
r.Header.Add("X-Forwarded-Method", method)
r.Header.Add("X-Forwarded-Host", p.Host)
r.Header.Add("X-Forwarded-Uri", uri) r.Header.Add("X-Forwarded-Uri", uri)
return r return r
} }

View File

@ -1,3 +1,5 @@
cookie-name=inicookiename cookie-name=inicookiename
csrf-cookie-name=inicsrfcookiename csrf-cookie-name=inicsrfcookiename
url-path=one url-path=one
rule.1.action=allow
rule.1.rule=PathPrefix(`/one`)

View File

@ -1 +1,3 @@
url-path=two url-path=two
rule.two.action=auth
rule.two.rule=Host(`two.com`) && Path(`/two`)