Skip to content

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

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

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
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
@@ -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:
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
26 changes: 13 additions & 13 deletions internal/auth.go
Original file line number Diff line number Diff line change
@@ -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, "|")

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

@@ -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
}
@@ -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,
80 changes: 48 additions & 32 deletions internal/auth_test.go
Original file line number Diff line number Diff line change
@@ -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

@@ -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
@@ -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
@@ -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
@@ -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")
}

8 changes: 4 additions & 4 deletions internal/config.go
Original file line number Diff line number Diff line change
@@ -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\""`
@@ -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
}

11 changes: 11 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
@@ -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{
Loading