Skip to content

Commit

Permalink
Merge pull request #113 from tahoward/email-addresses
Browse files Browse the repository at this point in the history
sso-auth: add provider for individual e-mail address authentication
  • Loading branch information
Benjamin Stockwell authored Feb 25, 2019
2 parents b63aad4 + f9b08b4 commit 683727d
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 10 deletions.
6 changes: 5 additions & 1 deletion cmd/sso-auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ func main() {
}

emailValidator := func(p *auth.Authenticator) error {
p.Validator = options.NewEmailValidator(opts.EmailDomains)
if len(opts.EmailAddresses) != 0 {
p.Validator = options.NewEmailAddressValidator(opts.EmailAddresses)
} else {
p.Validator = options.NewEmailDomainValidator(opts.EmailDomains)
}
return nil
}

Expand Down
6 changes: 5 additions & 1 deletion cmd/sso-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ func main() {
}

validator := func(p *proxy.OAuthProxy) error {
p.EmailValidator = options.NewEmailValidator(opts.EmailDomains)
if len(opts.EmailAddresses) != 0 {
p.EmailValidator = options.NewEmailAddressValidator(opts.EmailAddresses)
} else {
p.EmailValidator = options.NewEmailDomainValidator(opts.EmailDomains)
}
return nil
}

Expand Down
6 changes: 4 additions & 2 deletions internal/auth/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
// Host - string - The host that is in the header that is required on incoming requests
// Port - string - Port to listen on
// EmailDomains - []string - authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
// EmailAddresses - []string - authenticate emails with the specified email address (may be given multiple times). Use * to authenticate any email
// ProxyRootDomains - []string - only redirect to specified proxy domains (may be given multiple times)
// GoogleAdminEmail - string - the google admin to impersonate for api calls
// GoogleServiceAccountJSON - string - the path to the service account json credentials
Expand Down Expand Up @@ -61,6 +62,7 @@ type Options struct {
Port int `envconfig:"PORT" default:"4180"`

EmailDomains []string `envconfig:"SSO_EMAIL_DOMAIN"`
EmailAddresses []string `envconfig:"SSO_EMAIL_ADDRESSES"`
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`

GoogleAdminEmail string `envconfig:"GOOGLE_ADMIN_EMAIL"`
Expand Down Expand Up @@ -154,8 +156,8 @@ func (o *Options) Validate() error {
if o.ClientSecret == "" {
msgs = append(msgs, "missing setting: client-secret")
}
if len(o.EmailDomains) == 0 {
msgs = append(msgs, "missing setting for email validation: email-domain required.\n use email-domain=* to authorize all email addresses")
if len(o.EmailDomains) == 0 && len(o.EmailAddresses) == 0 {
msgs = append(msgs, "missing setting for email validation: email-domain or email-address required.\n use email-domain=* to authorize all email addresses")
}
if len(o.ProxyRootDomains) == 0 {
msgs = append(msgs, "missing setting: proxy-root-domain")
Expand Down
35 changes: 35 additions & 0 deletions internal/pkg/options/email_address_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package options

import (
"fmt"
"strings"
)

// NewEmailAddressValidator returns a function that checks whether a given email is valid based on a list
// of email addresses. The address "*" is a wild card that matches any non-empty email.
func NewEmailAddressValidator(emails []string) func(string) bool {
allowAll := false
for i, email := range emails {
if email == "*" {
allowAll = true
}
emails[i] = fmt.Sprintf("%s", strings.ToLower(email))
}

if allowAll {
return func(email string) bool { return email != "" }
}

return func(email string) bool {
if email == "" {
return false
}
email = strings.ToLower(email)
for _, emailItem := range emails {
if email == emailItem {
return true
}
}
return false
}
}
121 changes: 121 additions & 0 deletions internal/pkg/options/email_address_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package options

import (
"testing"
)

func TestEmailAddressValidatorValidator(t *testing.T) {
testCases := []struct {
name string
domains []string
email string
expectValid bool
}{
{
name: "nothing should validate when address list is empty",
domains: []string(nil),
email: "foo@example.com",
expectValid: false,
},
{
name: "single address validation",
domains: []string{"foo@example.com"},
email: "foo@example.com",
expectValid: true,
},
{
name: "substring matches are rejected",
domains: []string{"foo@example.com"},
email: "foo@hackerexample.com",
expectValid: false,
},
{
name: "no subdomain rollup happens",
domains: []string{"foo@example.com"},
email: "foo@bar.example.com",
expectValid: false,
},
{
name: "multiple address validation still rejects other addresses",
domains: []string{"foo@abc.com", "foo@xyz.com"},
email: "foo@example.com",
expectValid: false,
},
{
name: "multiple address validation still accepts emails from either address",
domains: []string{"foo@abc.com", "foo@xyz.com"},
email: "foo@abc.com",
expectValid: true,
},
{
name: "multiple address validation still rejects other addresses",
domains: []string{"foo@abc.com", "bar@xyz.com"},
email: "bar@xyz.com",
expectValid: true,
},
{
name: "comparisons are case insensitive",
domains: []string{"Foo@Example.Com"},
email: "foo@example.com",
expectValid: true,
},
{
name: "comparisons are case insensitive",
domains: []string{"Foo@Example.Com"},
email: "foo@EXAMPLE.COM",
expectValid: true,
},
{
name: "comparisons are case insensitive",
domains: []string{"foo@example.com"},
email: "foo@ExAmPlE.CoM",
expectValid: true,
},
{
name: "single wildcard allows all",
domains: []string{"*"},
email: "foo@example.com",
expectValid: true,
},
{
name: "single wildcard allows all",
domains: []string{"*"},
email: "bar@gmail.com",
expectValid: true,
},
{
name: "wildcard in list allows all",
domains: []string{"foo@example.com", "*"},
email: "foo@example.com",
expectValid: true,
},
{
name: "wildcard in list allows all",
domains: []string{"foo@example.com", "*"},
email: "foo@gmail.com",
expectValid: true,
},
{
name: "empty email rejected",
domains: []string{"foo@example.com"},
email: "",
expectValid: false,
},
{
name: "wildcard still rejects empty emails",
domains: []string{"*"},
email: "",
expectValid: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
emailValidator := NewEmailAddressValidator(tc.domains)
valid := emailValidator(tc.email)
if valid != tc.expectValid {
t.Fatalf("expected %v, got %v", tc.expectValid, valid)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"strings"
)

// NewEmailValidator returns a function that checks whether a given email is valid based on a list
// NewEmailDomainValidator returns a function that checks whether a given email is valid based on a list
// of domains. The domain "*" is a wild card that matches any non-empty email.
func NewEmailValidator(domains []string) func(string) bool {
func NewEmailDomainValidator(domains []string) func(string) bool {
allowAll := false
for i, domain := range domains {
if domain == "*" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"
)

func TestEmailValidatorValidator(t *testing.T) {
func TestEmailDomainValidatorValidator(t *testing.T) {
testCases := []struct {
name string
domains []string
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestEmailValidatorValidator(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
emailValidator := NewEmailValidator(tc.domains)
emailValidator := NewEmailDomainValidator(tc.domains)
valid := emailValidator(tc.email)
if valid != tc.expectValid {
t.Fatalf("expected %v, got %v", tc.expectValid, valid)
Expand Down
6 changes: 4 additions & 2 deletions internal/proxy/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
// Scheme - the default scheme, used for upstream configs
// SkipAuthPreflight - will skip authentication for OPTIONS requests, default false
// EmailDomains - csv list of emails with the specified domain to authenticate. Use * to authenticate any email
// EmailAddresses - []string - authenticate emails with the specified email address (may be given multiple times). Use * to authenticate any email
// DefaultAllowedGroups - csv list of default allowed groups that are applied to authorize access to upstreams. Will be overriden by groups specified in upstream configs.
// ClientID - the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
// ClientSecret - The OAuth Client Secret
Expand Down Expand Up @@ -59,6 +60,7 @@ type Options struct {
SkipAuthPreflight bool `envconfig:"SKIP_AUTH_PREFLIGHT"`

EmailDomains []string `envconfig:"EMAIL_DOMAIN"`
EmailAddresses []string `envconfig:"EMAIL_ADDRESSES"`
DefaultAllowedGroups []string `envconfig:"DEFAULT_ALLOWED_GROUPS"`

ClientID string `envconfig:"CLIENT_ID"`
Expand Down Expand Up @@ -150,8 +152,8 @@ func (o *Options) Validate() error {
if o.ClientSecret == "" {
msgs = append(msgs, "missing setting: client-secret")
}
if len(o.EmailDomains) == 0 {
msgs = append(msgs, "missing setting: email-domain")
if len(o.EmailDomains) == 0 && len(o.EmailAddresses) == 0 {
msgs = append(msgs, "missing setting: email-domain or email-address")
}

if o.StatsdHost == "" {
Expand Down
8 changes: 8 additions & 0 deletions quickstart/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ services:
# Allow any google account to log in for demo purposes
- EMAIL_DOMAIN=*

# (Optional) Allow specific google email address to log in for demo purposes
# This overrides EMAIL_DOMAIN
# - EMAIL_ADDRESSES=*

- UPSTREAM_CONFIGS=/sso/upstream_configs.yml
- PROVIDER_URL=http://sso-auth.localtest.me
- PROVIDER_URL_INTERNAL=http://host.docker.internal
Expand Down Expand Up @@ -68,6 +72,10 @@ services:
# Allow any google account to log in for demo purposes
- SSO_EMAIL_DOMAIN=*

# (Optional) Allow specific google email address to log in for demo purposes
# This overrides SSO_EMAIL_DOMAIN
# - SSO_EMAIL_ADDRESSES=*

- HOST=sso-auth.localtest.me
- REDIRECT_URL=http://sso-auth.localtest.me
- PROXY_ROOT_DOMAIN=localtest.me
Expand Down

0 comments on commit 683727d

Please sign in to comment.