Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom key to be used for whitelist and X-Forwarded-User instead of the hardcoded email #159

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Traefik Forward Auth
on: [push]
jobs:
test:
name: Test with Go version -
runs-on: ubuntu-latest

strategy:
matrix:
go: ['1.12', '1.13', '1.14']

steps:
- uses: actions/checkout@v1

- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go }}

- name: Run Tests
run: go test ./...

publish:
name: Publish Docker image
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
with:
fetch-depth: '0'
- name: Publish to Docker Registry
uses: docker/build-push-action@v1
with:
repository: ${{ github.repository }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tag_with_ref: true
tag_with_sha: true

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,12 @@ Application Options:
--csrf-cookie-name= CSRF Cookie Name (default: _forward_auth_csrf) [$CSRF_COOKIE_NAME]
--default-action=[auth|allow] Default action (default: auth) [$DEFAULT_ACTION]
--default-provider=[google|oidc|generic-oauth] Default provider (default: google) [$DEFAULT_PROVIDER]
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
--domain= Only allow given email domains, comma separated, can be set multiple times [$DOMAIN]
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
--logout-redirect= URL to redirect to following logout [$LOGOUT_REDIRECT]
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
--secret= Secret used for signing (required) [$SECRET]
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
--whitelist= Only allow given user ID, comma separated, can be set multiple times [$WHITELIST]
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"

Google Provider:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/thomseddon/traefik-forward-auth
go 1.13

require (
github.com/Jeffail/gabs/v2 v2.5.1
github.com/containous/traefik/v2 v2.1.2
github.com/coreos/go-oidc v2.1.0+incompatible
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/ExpediaDotCom/haystack-client-go v0.0.0-20190315171017-e7edbdf53a61/go.mod h1:62qWSDaEI0BLykU+zQza5CAKgW0lOy9oBSz3/DvYz4w=
github.com/Jeffail/gabs/v2 v2.5.1 h1:ANfZYjpMlfTTKebycu4X1AgkVWumFVDYQl7JwOr4mDk=
github.com/Jeffail/gabs/v2 v2.5.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
Expand Down
26 changes: 13 additions & 13 deletions internal/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// Request Validation

// ValidateCookie verifies that a cookie matches the expected format of:
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
// Cookie = hash(secret, cookie domain, user, expires)|expires|user
func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
parts := strings.Split(c.Value, "|")

Expand Down Expand Up @@ -56,10 +56,10 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
return parts[2], nil
}

// ValidateEmail checks if the given email address matches either a whitelisted
// email address, as defined by the "whitelist" config parameter. Or is part of
// ValidateUser checks if the given user matches either a whitelisted
// user, as defined by the "whitelist" config parameter. Or is part of
// a permitted domain, as defined by the "domains" config parameter
func ValidateEmail(email, ruleName string) bool {
func ValidateUser(user, ruleName string) bool {
// Use global config by default
whitelist := config.Whitelist
domains := config.Domains
Expand All @@ -79,7 +79,7 @@ func ValidateEmail(email, ruleName string) bool {

// Email whitelist validation
if len(whitelist) > 0 {
if ValidateWhitelist(email, whitelist) {
if ValidateWhitelist(user, whitelist) {
return true
}

Expand All @@ -90,26 +90,26 @@ func ValidateEmail(email, ruleName string) bool {
}

// Domain validation
if len(domains) > 0 && ValidateDomains(email, domains) {
if len(domains) > 0 && ValidateDomains(user, domains) {
return true
}

return false
}

// ValidateWhitelist checks if the email is in whitelist
func ValidateWhitelist(email string, whitelist CommaSeparatedList) bool {
func ValidateWhitelist(user string, whitelist CommaSeparatedList) bool {
for _, whitelist := range whitelist {
if email == whitelist {
if user == whitelist {
return true
}
}
return false
}

// ValidateDomains checks if the email matches a whitelisted domain
func ValidateDomains(email string, domains CommaSeparatedList) bool {
parts := strings.Split(email, "@")
func ValidateDomains(user string, domains CommaSeparatedList) bool {
parts := strings.Split(user, "@")
if len(parts) < 2 {
return false
}
Expand Down Expand Up @@ -167,10 +167,10 @@ func useAuthDomain(r *http.Request) (bool, string) {
// Cookie methods

// MakeCookie creates an auth cookie
func MakeCookie(r *http.Request, email string) *http.Cookie {
func MakeCookie(r *http.Request, user string) *http.Cookie {
expires := cookieExpiry()
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
value := fmt.Sprintf("%s|%d|%s", mac, expires.Unix(), email)
mac := cookieSignature(r, user, fmt.Sprintf("%d", expires.Unix()))
value := fmt.Sprintf("%s|%d|%s", mac, expires.Unix(), user)

return &http.Cookie{
Name: config.CookieName,
Expand Down
80 changes: 48 additions & 32 deletions internal/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,54 +61,66 @@ func TestAuthValidateCookie(t *testing.T) {
assert.Equal("[email protected]", email, "valid request should return user email")
}

func TestAuthValidateEmail(t *testing.T) {
func TestAuthValidateUser(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})

// Should allow any with no whitelist/domain is specified
v := ValidateEmail("[email protected]", "default")
v := ValidateUser("[email protected]", "default")
assert.True(v, "should allow any domain if email domain is not defined")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow any domain if email domain is not defined")

// Should allow matching domain
config.Domains = []string{"test.com"}
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user from another domain")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user from allowed domain")

// Should block non whitelisted email address
config.Domains = []string{}
config.Whitelist = []string{"[email protected]"}
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user not in whitelist")

// Should allow matching whitelisted email address
config.Domains = []string{}
config.Whitelist = []string{"[email protected]"}
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user not in whitelist")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user in whitelist")

// Should allow only matching email address when
// MatchWhitelistOrDomain is disabled
config.Domains = []string{"example.com"}
config.Whitelist = []string{"[email protected]"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user in whitelist")
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user from valid domain")
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user from allowed domain")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user in whitelist")

// Should allow either matching domain or email address when
// MatchWhitelistOrDomain is enabled
config.Domains = []string{"example.com"}
config.Whitelist = []string{"[email protected]"}
config.MatchWhitelistOrDomain = true
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user from allowed domain")
v = ValidateEmail("[email protected]", "default")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user in whitelist")
v = ValidateUser("[email protected]", "default")
assert.True(v, "should allow user from valid domain")

// Rule testing

Expand All @@ -117,11 +129,11 @@ func TestAuthValidateEmail(t *testing.T) {
config.Whitelist = []string{"[email protected]"}
config.Rules = map[string]*Rule{"test": NewRule()}
config.MatchWhitelistOrDomain = true
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user from allowed global domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user in global whitelist")

// Should allow matching domain in rule
Expand All @@ -131,25 +143,29 @@ func TestAuthValidateEmail(t *testing.T) {
config.Rules = map[string]*Rule{"test": rule}
rule.Domains = []string{"testrule.com"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from another domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user from allowed domain")

// Should allow comma separated email
config.Whitelist = []string{"[email protected]", "[email protected]"}
v = ValidateUser("[email protected]", "default")

// Should allow matching whitelist in rule
config.Domains = []string{}
config.Whitelist = []string{"[email protected]"}
rule = NewRule()
config.Rules = map[string]*Rule{"test": rule}
rule.Whitelist = []string{"[email protected]"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from another domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user from allowed domain")

// Should allow only matching email address when
Expand All @@ -161,15 +177,15 @@ func TestAuthValidateEmail(t *testing.T) {
rule.Domains = []string{"examplerule.com"}
rule.Whitelist = []string{"[email protected]"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user in global whitelist")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from allowed domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user in whitelist")

// Should allow either matching domain or email address when
Expand All @@ -181,15 +197,15 @@ func TestAuthValidateEmail(t *testing.T) {
rule.Domains = []string{"examplerule.com"}
rule.Whitelist = []string{"[email protected]"}
config.MatchWhitelistOrDomain = true
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user in global whitelist")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user from allowed domain")
v = ValidateEmail("[email protected]", "test")
v = ValidateUser("[email protected]", "test")
assert.True(v, "should allow user in whitelist")
}

Expand Down
8 changes: 4 additions & 4 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ type Config struct {
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" choice:"generic-oauth" description:"Default provider"`
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," 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, comma separated, can be set multiple times"`
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
MatchWhitelistOrDomain bool `long:"match-whitelist-or-domain" env:"MATCH_WHITELIST_OR_DOMAIN" description:"Allow users that match *either* whitelist or domain (enabled by default in v3)"`
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" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
UserPath string `long:"user-id-path" env:"USER_ID_PATH" default:"email" description:"Dot notation path of a UserID for use with whitelist and X-Forwarded-User"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given UserID, comma separated, 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\""`
Expand Down Expand Up @@ -325,8 +326,7 @@ func (c *Config) setupProvider(name string) error {
}

// Setup
err = p.Setup()
if err != nil {
if err := p.Setup(); err != nil {
return err
}

Expand Down
11 changes: 11 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ func TestConfigParseRuleError(t *testing.T) {
assert.Equal(map[string]*Rule{}, c.Rules)
}

func TestConfigCommaSeperated(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{
"[email protected],[email protected]",
})
require.Nil(t, err)

expected1 := CommaSeparatedList{"[email protected]", "[email protected]"}
assert.Equal(expected1, c.Whitelist, "should read legacy comma separated list whitelist")
}

func TestConfigFlagBackwardsCompatability(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{
Expand Down
Loading