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
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

1
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/sirupsen/logrus v1.4.1
github.com/stretchr/objx v0.2.0 // indirect
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
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd // 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.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
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/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

View File

@ -14,7 +14,7 @@ import (
"strings"
"time"
"github.com/jessevdk/go-flags"
"github.com/thomseddon/go-flags"
"github.com/thomseddon/traefik-forward-auth/internal/provider"
)
@ -100,7 +100,7 @@ func NewConfig(args []string) (Config, error) {
// Backwards compatability
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
}
if c.ClientIdLegacy != "" {
@ -110,11 +110,11 @@ func NewConfig(args []string) (Config, error) {
c.Providers.Google.ClientSecret = c.ClientSecretLegacy
}
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
}
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)
if err != nil {
return c, err
@ -122,11 +122,11 @@ func NewConfig(args []string) (Config, error) {
c.InsecureCookie = !secure
}
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...)
}
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...)
}
@ -141,7 +141,7 @@ func NewConfig(args []string) (Config, 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
i := flags.NewIniParser(p)
@ -157,6 +157,7 @@ func (c *Config) parseFlags(args []string) error {
return err
}
fmt.Println("config format deprecated, please use ini format")
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() {
if r.Action != "auth" && r.Action != "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("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(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) {

View File

@ -28,9 +28,9 @@ func (s *Server) buildRoutes() {
// Let's build a router
for name, rule := range config.Rules {
if rule.Action == "allow" {
s.router.AddRoute(rule.Rule, 1, s.AllowHandler(name))
s.router.AddRoute(rule.formattedRule(), 1, s.AllowHandler(name))
} 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) {
// 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"))
// Pass to mux

View File

@ -32,7 +32,7 @@ func TestServerAuthHandler(t *testing.T) {
config, _ = NewConfig([]string{})
// Should redirect vanilla request to login url
req := newHttpRequest("/foo")
req := newDefaultHttpRequest("/foo")
res, _ := doHttpRequest(req, nil)
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")
// Should catch invalid cookie
req = newHttpRequest("/foo")
req = newDefaultHttpRequest("/foo")
c := MakeCookie(req, "test@example.com")
parts := strings.Split(c.Value, "|")
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")
// Should validate email
req = newHttpRequest("/foo")
req = newDefaultHttpRequest("/foo")
c = MakeCookie(req, "test@example.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")
// Should allow valid request email
req = newHttpRequest("/foo")
req = newDefaultHttpRequest("/foo")
c = MakeCookie(req, "test@example.com")
config.Domains = []string{}
@ -91,18 +91,18 @@ func TestServerAuthCallback(t *testing.T) {
config.Providers.Google.UserURL = userUrl
// Should pass auth response request to callback
req := newHttpRequest("/_oauth")
req := newDefaultHttpRequest("/_oauth")
res, _ := doHttpRequest(req, nil)
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
// Should catch invalid csrf cookie
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
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 = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ = doHttpRequest(req, c)
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed")
@ -117,33 +117,151 @@ func TestServerDefaultAction(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
req := newHttpRequest("/random")
req := newDefaultHttpRequest("/random")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request should require auth with auth default handler")
config.DefaultAction = "allow"
req = newHttpRequest("/random")
req = newDefaultHttpRequest("/random")
res, _ = doHttpRequest(req, nil)
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)
config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{
"web1": {
"1": {
Action: "allow",
Rule: "PathPrefix(`/api`)",
Rule: "Headers(`X-Test`, `test123`)",
},
"2": {
Action: "allow",
Rule: "HeadersRegexp(`X-Test`, `test(456|789)`)",
},
}
// 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)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// 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)
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)
}
func newHttpRequest(uri string) *http.Request {
r := httptest.NewRequest("", "http://example.com/", nil)
func newDefaultHttpRequest(uri string) *http.Request {
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)
return r
}

View File

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

View File

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