Skip to content

Commit 9019d4f

Browse files
committed
sso_*: rewrite validator abstractions
working with list, not struct struct testing - think it works? general kind of working with returning Result struct new validator structure adding "<validator_name>:err msg" log line some unfinished auth changes add validators file adding more sso_auth changes fixing up some errors fix some formatting and bugs fix validators in structs, and simplify logging tests for email address and domain validtors just return errors fix func name and capitalization sso_auth: validator tests func name sso_proxy: return correct error sso_proxy: validator tests mock validator misc adding some docstrings this test was pulled in from a different branch and is unrelated add some more context to fail states extra comments authenticator tests
1 parent b490092 commit 9019d4f

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)