Skip to content

Commit 70d38bc

Browse files
authored
Merge pull request #253 from buzzfeed/jusshersmith-OR-statement-allowed-email-settings
sso_*: allow simultaneous use of Validators
2 parents f426749 + 9019d4f commit 70d38bc

14 files changed

+529
-279
lines changed

internal/auth/authenticator.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/buzzfeed/sso/internal/auth/providers"
1313
"github.com/buzzfeed/sso/internal/pkg/aead"
1414
log "github.com/buzzfeed/sso/internal/pkg/logging"
15+
"github.com/buzzfeed/sso/internal/pkg/options"
1516
"github.com/buzzfeed/sso/internal/pkg/sessions"
1617
"github.com/buzzfeed/sso/internal/pkg/templates"
1718

@@ -20,7 +21,7 @@ import (
2021

2122
// Authenticator stores all the information associated with proxying the request.
2223
type Authenticator struct {
23-
Validator func(string) bool
24+
Validators []options.Validator
2425
EmailDomains []string
2526
ProxyRootDomains []string
2627
Host string
@@ -225,11 +226,16 @@ func (p *Authenticator) authenticate(rw http.ResponseWriter, req *http.Request)
225226
}
226227
}
227228

228-
if !p.Validator(session.Email) {
229-
logger.WithUser(session.Email).Error("invalid email user")
229+
errors := options.RunValidators(p.Validators, session)
230+
if len(errors) == len(p.Validators) {
231+
logger.WithUser(session.Email).Info(
232+
fmt.Sprintf("permission denied: unauthorized: %q", errors))
230233
return nil, ErrUserNotAuthorized
231234
}
232235

236+
logger.WithRemoteAddress(remoteAddr).WithUser(session.Email).Info(
237+
fmt.Sprintf("authentication: user passed validation"))
238+
233239
return session, nil
234240
}
235241

@@ -575,13 +581,24 @@ func (p *Authenticator) getOAuthCallback(rw http.ResponseWriter, req *http.Reque
575581
// Set cookie, or deny: The authenticator validates the session email and group
576582
// - for p.Validator see validator.go#newValidatorImpl for more info
577583
// - for p.provider.ValidateGroup see providers/google.go#ValidateGroup for more info
578-
if !p.Validator(session.Email) {
584+
585+
errors := options.RunValidators(p.Validators, session)
586+
if len(errors) == len(p.Validators) {
579587
tags := append(tags, "error:invalid_email")
580588
p.StatsdClient.Incr("application_error", tags, 1.0)
581-
logger.WithRemoteAddress(remoteAddr).WithUser(session.Email).Error(
582-
"invalid_email", "permission denied; unauthorized user")
583-
return "", HTTPError{Code: http.StatusForbidden, Message: "Invalid Account"}
589+
logger.WithRemoteAddress(remoteAddr).WithUser(session.Email).Info(
590+
fmt.Sprintf("oauth callback: unauthorized: %q", errors))
591+
592+
formattedErrors := make([]string, 0, len(errors))
593+
for _, err := range errors {
594+
formattedErrors = append(formattedErrors, err.Error())
595+
}
596+
errorMsg := fmt.Sprintf("We ran into some issues while validating your account: \"%s\"",
597+
strings.Join(formattedErrors, ", "))
598+
return "", HTTPError{Code: http.StatusForbidden, Message: errorMsg}
584599
}
600+
logger.WithRemoteAddress(remoteAddr).WithUser(session.Email).Info(
601+
fmt.Sprintf("oauth callback: user passed validation"))
585602

586603
logger.WithRemoteAddress(remoteAddr).WithUser(session.Email).Info("authentication complete")
587604
err = p.sessionStore.SaveSession(rw, req, session)

internal/auth/authenticator_test.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/buzzfeed/sso/internal/auth/providers"
1919
"github.com/buzzfeed/sso/internal/pkg/aead"
20+
"github.com/buzzfeed/sso/internal/pkg/options"
2021
"github.com/buzzfeed/sso/internal/pkg/sessions"
2122
"github.com/buzzfeed/sso/internal/pkg/templates"
2223
"github.com/buzzfeed/sso/internal/pkg/testutil"
@@ -66,13 +67,6 @@ func setTestProvider(provider *providers.TestProvider) func(*Authenticator) erro
6667
}
6768
}
6869

69-
func setMockValidator(response bool) func(*Authenticator) error {
70-
return func(a *Authenticator) error {
71-
a.Validator = func(string) bool { return response }
72-
return nil
73-
}
74-
}
75-
7670
func setRedirectURL(redirectURL *url.URL) func(*Authenticator) error {
7771
return func(a *Authenticator) error {
7872
a.redirectURL = redirectURL
@@ -424,7 +418,7 @@ func TestSignIn(t *testing.T) {
424418
t.Run(tc.name, func(t *testing.T) {
425419
config := testConfiguration(t)
426420
auth, err := NewAuthenticator(config,
427-
setMockValidator(tc.validEmail),
421+
SetValidators([]options.Validator{options.NewMockValidator(tc.validEmail)}),
428422
setMockSessionStore(tc.mockSessionStore),
429423
setMockTempl(),
430424
setMockRedirectURL(),
@@ -571,7 +565,7 @@ func TestSignOutPage(t *testing.T) {
571565
provider.RevokeError = tc.RevokeError
572566

573567
p, _ := NewAuthenticator(config,
574-
setMockValidator(true),
568+
SetValidators([]options.Validator{options.NewMockValidator(true)}),
575569
setMockSessionStore(tc.mockSessionStore),
576570
setMockTempl(),
577571
setTestProvider(provider),
@@ -948,7 +942,7 @@ func TestGetProfile(t *testing.T) {
948942
t.Run(tc.name, func(t *testing.T) {
949943
config := testConfiguration(t)
950944
p, _ := NewAuthenticator(config,
951-
setMockValidator(true),
945+
SetValidators([]options.Validator{options.NewMockValidator(true)}),
952946
)
953947
u, _ := url.Parse("http://example.com")
954948
testProvider := providers.NewTestProvider(u)
@@ -1050,7 +1044,7 @@ func TestRedeemCode(t *testing.T) {
10501044
config := testConfiguration(t)
10511045

10521046
proxy, _ := NewAuthenticator(config,
1053-
setMockValidator(true),
1047+
SetValidators([]options.Validator{options.NewMockValidator(true)}),
10541048
)
10551049

10561050
testURL, err := url.Parse("example.com")
@@ -1357,7 +1351,8 @@ func TestOAuthCallback(t *testing.T) {
13571351
Value: "state",
13581352
},
13591353
},
1360-
expectedError: HTTPError{Code: http.StatusForbidden, Message: "Invalid Account"},
1354+
expectedError: HTTPError{Code: http.StatusForbidden,
1355+
Message: "We ran into some issues while validating your account: \"MockValidator error\""},
13611356
},
13621357
{
13631358
name: "valid email, invalid redirect",
@@ -1438,7 +1433,7 @@ func TestOAuthCallback(t *testing.T) {
14381433
t.Run(tc.name, func(t *testing.T) {
14391434
config := testConfiguration(t)
14401435
proxy, _ := NewAuthenticator(config,
1441-
setMockValidator(tc.validEmail),
1436+
SetValidators([]options.Validator{options.NewMockValidator(tc.validEmail)}),
14421437
setMockCSRFStore(tc.csrfResp),
14431438
setMockSessionStore(tc.sessionStore),
14441439
)
@@ -1559,7 +1554,7 @@ func TestOAuthStart(t *testing.T) {
15591554
provider := providers.NewTestProvider(nil)
15601555
proxy, _ := NewAuthenticator(config,
15611556
setTestProvider(provider),
1562-
setMockValidator(true),
1557+
SetValidators([]options.Validator{options.NewMockValidator(true)}),
15631558
setMockRedirectURL(),
15641559
setMockCSRFStore(&sessions.MockCSRFStore{}),
15651560
)

internal/auth/mux.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@ type AuthenticatorMux struct {
1818

1919
func NewAuthenticatorMux(config Configuration, statsdClient *statsd.Client) (*AuthenticatorMux, error) {
2020
logger := log.NewLogEntry()
21-
22-
var validator func(string) bool
21+
validators := []options.Validator{}
2322
if len(config.AuthorizeConfig.EmailConfig.Addresses) != 0 {
24-
validator = options.NewEmailAddressValidator(config.AuthorizeConfig.EmailConfig.Addresses)
23+
validators = append(validators, options.NewEmailAddressValidator(config.AuthorizeConfig.EmailConfig.Addresses))
2524
} else {
26-
validator = options.NewEmailDomainValidator(config.AuthorizeConfig.EmailConfig.Domains)
25+
validators = append(validators, options.NewEmailDomainValidator(config.AuthorizeConfig.EmailConfig.Domains))
2726
}
2827

2928
authenticators := []*Authenticator{}
@@ -38,7 +37,7 @@ func NewAuthenticatorMux(config Configuration, statsdClient *statsd.Client) (*Au
3837

3938
idpSlug := idp.Data().ProviderSlug
4039
authenticator, err := NewAuthenticator(config,
41-
SetValidator(validator),
40+
SetValidators(validators),
4241
SetProvider(idp),
4342
SetCookieStore(config.SessionConfig, idpSlug),
4443
SetStatsdClient(statsdClient),

internal/auth/options.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/buzzfeed/sso/internal/auth/providers"
1010
"github.com/buzzfeed/sso/internal/pkg/aead"
1111
"github.com/buzzfeed/sso/internal/pkg/groups"
12+
"github.com/buzzfeed/sso/internal/pkg/options"
1213
"github.com/buzzfeed/sso/internal/pkg/sessions"
1314

1415
"github.com/datadog/datadog-go/statsd"
@@ -96,9 +97,9 @@ func SetRedirectURL(serverConfig ServerConfig, slug string) func(*Authenticator)
9697
}
9798

9899
// SetValidator sets the email validator
99-
func SetValidator(validator func(string) bool) func(*Authenticator) error {
100+
func SetValidators(validators []options.Validator) func(*Authenticator) error {
100101
return func(a *Authenticator) error {
101-
a.Validator = validator
102+
a.Validators = validators
102103
return nil
103104
}
104105
}
Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,72 @@
11
package options
22

33
import (
4+
"errors"
45
"fmt"
56
"strings"
7+
8+
"github.com/buzzfeed/sso/internal/pkg/sessions"
69
)
710

8-
// NewEmailAddressValidator returns a function that checks whether a given email is valid based on a list
9-
// of email addresses. The address "*" is a wild card that matches any non-empty email.
10-
func NewEmailAddressValidator(emails []string) func(string) bool {
11-
allowAll := false
11+
var (
12+
_ Validator = EmailAddressValidator{}
13+
14+
// These error message should be formatted in such a way that is appropriate
15+
// for display to the end user.
16+
ErrEmailAddressDenied = errors.New("Unauthorized Email Address")
17+
)
18+
19+
type EmailAddressValidator struct {
20+
AllowedEmails []string
21+
}
22+
23+
// NewEmailAddressValidator takes in a list of email addresses and returns a Validator object.
24+
// The validator can be used to validate that the session.Email:
25+
// - is non-empty
26+
// - matches one of the originally passed in email addresses
27+
// (case insensitive)
28+
// - if the originally passed in list of emails consists only of "*", then all emails
29+
// are considered valid based on their domain.
30+
// If valid, nil is returned in place of an error.
31+
func NewEmailAddressValidator(allowedEmails []string) EmailAddressValidator {
1232
var emailAddresses []string
1333

14-
for _, email := range emails {
15-
if email == "*" {
16-
allowAll = true
17-
}
34+
for _, email := range allowedEmails {
1835
emailAddress := fmt.Sprintf("%s", strings.ToLower(email))
1936
emailAddresses = append(emailAddresses, emailAddress)
2037
}
2138

22-
if allowAll {
23-
return func(email string) bool { return email != "" }
39+
return EmailAddressValidator{
40+
AllowedEmails: emailAddresses,
2441
}
42+
}
2543

26-
return func(email string) bool {
27-
if email == "" {
28-
return false
29-
}
30-
email = strings.ToLower(email)
31-
for _, emailItem := range emailAddresses {
32-
if email == emailItem {
33-
return true
34-
}
44+
func (v EmailAddressValidator) Validate(session *sessions.SessionState) error {
45+
if session.Email == "" {
46+
return ErrInvalidEmailAddress
47+
}
48+
49+
if len(v.AllowedEmails) == 0 {
50+
return ErrEmailAddressDenied
51+
}
52+
53+
if len(v.AllowedEmails) == 1 && v.AllowedEmails[0] == "*" {
54+
return nil
55+
}
56+
57+
err := v.validate(session)
58+
if err != nil {
59+
return err
60+
}
61+
return nil
62+
}
63+
64+
func (v EmailAddressValidator) validate(session *sessions.SessionState) error {
65+
email := strings.ToLower(session.Email)
66+
for _, emailItem := range v.AllowedEmails {
67+
if email == emailItem {
68+
return nil
3569
}
36-
return false
3770
}
71+
return ErrEmailAddressDenied
3872
}

0 commit comments

Comments
 (0)