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 19 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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.13-alpine as builder
FROM golang:1.14-alpine as builder
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking, these version updates would normally not be part of a pull request.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean? Only @thomseddon should make these changes?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's completely up to the maintainer, but in many projects I contributed to, a PR was really just about one specific feature or bugfix.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted


# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.arm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.13-alpine as builder
FROM golang:1.14-alpine as builder

# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.arm64
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.13-alpine as builder
FROM golang:1.14-alpine as builder

# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
Expand Down
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 email addresses, comma separated, can be set multiple times [$WHITELIST]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should now also be

-Only allow given email addresses
+Only allow given user id

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, good catch!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"

Google Provider:
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module github.com/thomseddon/traefik-forward-auth

go 1.13
go 1.14

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
20 changes: 10 additions & 10 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 string) bool {
func ValidateUser(user string) bool {
// Do we have any validation to perform?
if len(config.Whitelist) == 0 && len(config.Domains) == 0 {
return true
Expand All @@ -68,7 +68,7 @@ func ValidateEmail(email string) bool {
// Email whitelist validation
if len(config.Whitelist) > 0 {
for _, whitelist := range config.Whitelist {
if email == whitelist {
if user == whitelist {
return true
}
}
Expand All @@ -81,7 +81,7 @@ func ValidateEmail(email string) bool {

// Domain validation
if len(config.Domains) > 0 {
parts := strings.Split(email, "@")
parts := strings.Split(user, "@")
if len(parts) < 2 {
return false
}
Expand Down Expand Up @@ -141,10 +141,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 Expand Up @@ -350,7 +350,7 @@ func (c *CookieDomains) UnmarshalFlag(value string) error {
return nil
}

// MarshalFlag converts an array of CookieDomain to a comma seperated list
// MarshalFlag converts an array of CookieDomain to a comma separated list
func (c *CookieDomains) MarshalFlag() (string, error) {
var domains []string
for _, d := range *c {
Expand Down
29 changes: 17 additions & 12 deletions internal/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,56 +67,61 @@ func TestAuthValidateEmail(t *testing.T) {
config, _ = NewConfig([]string{})

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

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

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

// Should block non whitelisted email address
config.Domains = []string{}
config.Whitelist = []string{"[email protected]"}
v = ValidateEmail("[email protected]")
v = ValidateUser("[email protected]")
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]")
v = ValidateUser("[email protected]")
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]")
v = ValidateUser("[email protected]")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("[email protected]")
v = ValidateUser("[email protected]")
assert.False(v, "should not allow user from valid domain")
v = ValidateEmail("[email protected]")
v = ValidateUser("[email protected]")
assert.False(v, "should not allow user not in either")

// 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]")
v = ValidateUser("[email protected]")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("[email protected]")
v = ValidateUser("[email protected]")
assert.True(v, "should allow user from valid domain")
v = ValidateEmail("[email protected]")
v = ValidateUser("[email protected]")
assert.False(v, "should not allow user not in either")

// Should allow comma separated email
config.Whitelist = []string{"[email protected]", "[email protected]"}
v = ValidateUser("[email protected]")
assert.True(v, "should allow user in whitelist")
}

func TestRedirectUri(t *testing.T) {
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 email addresses, 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 @@ -317,8 +318,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
15 changes: 5 additions & 10 deletions internal/provider/generic_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package provider

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -66,13 +65,11 @@ func (o *GenericOAuth) ExchangeCode(redirectURI, code string) (string, error) {
return token.AccessToken, nil
}

// GetUser uses the given token and returns a complete provider.User object
func (o *GenericOAuth) GetUser(token string) (User, error) {
var user User

// GetUser uses the given token and returns a UserID
func (o *GenericOAuth) GetUser(token, UserPath string) (string, error) {
req, err := http.NewRequest("GET", o.UserURL, nil)
if err != nil {
return user, err
return "", err
}

if o.TokenStyle == "header" {
Expand All @@ -86,11 +83,9 @@ func (o *GenericOAuth) GetUser(token string) (User, error) {
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return user, err
return "", err
}

defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&user)

return user, err
return GetUser(res.Body, UserPath)
}
4 changes: 2 additions & 2 deletions internal/provider/generic_oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ func TestGenericOAuthGetUser(t *testing.T) {
// AuthStyleInHeader is attempted
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams

user, err := p.GetUser("123456789")
user, err := p.GetUser("123456789", "email")
assert.Nil(err)

assert.Equal("[email protected]", user.Email)
assert.Equal("[email protected]", user)
}
14 changes: 5 additions & 9 deletions internal/provider/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,20 @@ func (g *Google) ExchangeCode(redirectURI, code string) (string, error) {
return token.Token, err
}

// GetUser uses the given token and returns a complete provider.User object
func (g *Google) GetUser(token string) (User, error) {
var user User

// GetUser uses the given token and returns a userID located at the json path
func (g *Google) GetUser(token, UserPath string) (string, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", g.UserURL.String(), nil)
if err != nil {
return user, err
return "", err
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
res, err := client.Do(req)
if err != nil {
return user, err
return "", err
}

defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&user)

return user, err
return GetUser(res.Body, UserPath)
}
4 changes: 2 additions & 2 deletions internal/provider/google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ func TestGoogleGetUser(t *testing.T) {
},
}

user, err := p.GetUser("123456789")
user, err := p.GetUser("123456789", "email")
assert.Nil(err)

assert.Equal("[email protected]", user.Email)
assert.Equal("[email protected]", user)
}
12 changes: 5 additions & 7 deletions internal/provider/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package provider
import (
"context"
"errors"

"github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
Expand Down Expand Up @@ -81,19 +80,18 @@ func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
}

// GetUser uses the given token and returns a complete provider.User object
func (o *OIDC) GetUser(token string) (User, error) {
var user User

func (o *OIDC) GetUser(token, _ string) (string, error) {
// Parse & Verify ID Token
idToken, err := o.verifier.Verify(o.ctx, token)
if err != nil {
return user, err
return "", err
}

// Extract custom claims
var user User
if err := idToken.Claims(&user); err != nil {
return user, err
return "", err
}

return user, nil
return user.Email, nil
}
6 changes: 3 additions & 3 deletions internal/provider/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/stretchr/testify/assert"
jose "gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2"
)

// Tests
Expand Down Expand Up @@ -122,9 +122,9 @@ func TestOIDCGetUser(t *testing.T) {
}`))

// Get user
user, err := provider.GetUser(token)
user, err := provider.GetUser(token, "")
assert.Nil(err)
assert.Equal("[email protected]", user.Email)
assert.Equal("[email protected]", user)
}

// Utils
Expand Down
Loading