Skip to content

Commit

Permalink
feat: add authorized email address support
Browse files Browse the repository at this point in the history
  • Loading branch information
hf committed Sep 2, 2024
1 parent 2ad0737 commit 738e079
Show file tree
Hide file tree
Showing 14 changed files with 85 additions and 27 deletions.
4 changes: 2 additions & 2 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error {
}

if params.Email != "" {
params.Email, err = validateEmail(params.Email)
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
Expand Down Expand Up @@ -343,7 +343,7 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error {

var providers []string
if params.Email != "" {
params.Email, err = validateEmail(params.Email)
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion internal/api/errorcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,6 @@ const (
ErrorCodeMFATOTPVerifyDisabled ErrorCode = "mfa_totp_verify_not_enabled"
ErrorCodeMFAVerifiedFactorExists ErrorCode = "mfa_verified_factor_exists"
//#nosec G101 -- Not a secret value.
ErrorCodeInvalidCredentials ErrorCode = "invalid_credentials"
ErrorCodeInvalidCredentials ErrorCode = "invalid_credentials"
ErrorCodeEmailAddressNotAuthorized ErrorCode = "email_address_not_authorized"
)
2 changes: 1 addition & 1 deletion internal/api/invite.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error {
}

var err error
params.Email, err = validateEmail(params.Email)
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions internal/api/magic_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ type MagicLinkParams struct {
CodeChallenge string `json:"code_challenge"`
}

func (p *MagicLinkParams) Validate() error {
func (p *MagicLinkParams) Validate(a *API) error {
if p.Email == "" {
return unprocessableEntityError(ErrorCodeValidationFailed, "Password recovery requires an email")
}
var err error
p.Email, err = validateEmail(p.Email)
p.Email, err = a.validateEmail(p.Email)
if err != nil {
return err
}
Expand Down Expand Up @@ -57,7 +57,7 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error {
return badRequestError(ErrorCodeBadJSON, "Could not read verification params: %v", err).WithInternalError(err)
}

if err := params.Validate(); err != nil {
if err := params.Validate(a); err != nil {
return err
}

Expand Down
28 changes: 24 additions & 4 deletions internal/api/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"net/http"
"regexp"
"strings"
"time"

Expand All @@ -24,6 +25,7 @@ import (

var (
EmailRateLimitExceeded error = errors.New("email rate limit exceeded")
AddressNotAuthorized error = errors.New("Destination email address not authorized")
)

type GenerateLinkParams struct {
Expand Down Expand Up @@ -56,7 +58,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
}

var err error
params.Email, err = validateEmail(params.Email)
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
Expand Down Expand Up @@ -230,7 +232,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
if !config.Mailer.SecureEmailChangeEnabled && params.Type == "email_change_current" {
return badRequestError(ErrorCodeValidationFailed, "Enable secure email change to generate link for current email")
}
params.NewEmail, terr = validateEmail(params.NewEmail)
params.NewEmail, terr = a.validateEmail(params.NewEmail)
if terr != nil {
return terr
}
Expand Down Expand Up @@ -548,7 +550,9 @@ func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models
return nil
}

func validateEmail(email string) (string, error) {
var emailLabelPattern = regexp.MustCompile("[+][^@]+@")

func (a *API) validateEmail(email string) (string, error) {
if email == "" {
return "", badRequestError(ErrorCodeValidationFailed, "An email address is required")
}
Expand All @@ -558,7 +562,23 @@ func validateEmail(email string) (string, error) {
if err := checkmail.ValidateFormat(email); err != nil {
return "", badRequestError(ErrorCodeValidationFailed, "Unable to validate email address: "+err.Error())
}
return strings.ToLower(email), nil

email = strings.ToLower(email)

if len(a.config.External.Email.AuthorizedAddresses) > 0 {
// allow labelled emails when authorization rules are in place
normalized := emailLabelPattern.ReplaceAllString(email, "@")

for _, authorizedAddress := range a.config.External.Email.AuthorizedAddresses {
if normalized == authorizedAddress {
return email, nil
}
}

return "", badRequestError(ErrorCodeEmailAddressNotAuthorized, "Email address %q cannot be used as it is not authorized", email)
}

return email, nil
}

func validateSentWithinFrequencyLimit(sentAt *time.Time, frequency time.Duration) error {
Expand Down
35 changes: 35 additions & 0 deletions internal/api/mail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,41 @@ func (ts *MailTestSuite) SetupTest() {
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new user")
}

func (ts *MailTestSuite) TestValidateEmailAuthorizedAddresses() {
ts.Config.External.Email.AuthorizedAddresses = []string{"someone-a@example.com", "someone-b@example.com"}
defer func() {
ts.Config.External.Email.AuthorizedAddresses = nil
}()

positiveExamples := []string{
"someone-a@example.com",
"someone-b@example.com",
"someone-a+test-1@example.com",
"someone-b+test-2@example.com",
"someone-A@example.com",
"someone-B@example.com",
"someone-a@Example.com",
"someone-b@Example.com",
}

negativeExamples := []string{
"someone@example.com",
"s.omeone@example.com",
"someone-a+@example.com",
"someone+a@example.com",
}

for _, example := range positiveExamples {
_, err := ts.API.validateEmail(example)
require.NoError(ts.T(), err)
}

for _, example := range negativeExamples {
_, err := ts.API.validateEmail(example)
require.Error(ts.T(), err)
}
}

func (ts *MailTestSuite) TestGenerateLink() {
// create admin jwt
claims := &AccessTokenClaims{
Expand Down
2 changes: 1 addition & 1 deletion internal/api/otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (a *API) shouldCreateUser(r *http.Request, params *OtpParams) (bool, error)
aud := a.requestAud(ctx, r)
var err error
if params.Email != "" {
params.Email, err = validateEmail(params.Email)
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return false, err
}
Expand Down
6 changes: 3 additions & 3 deletions internal/api/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ type RecoverParams struct {
CodeChallengeMethod string `json:"code_challenge_method"`
}

func (p *RecoverParams) Validate() error {
func (p *RecoverParams) Validate(a *API) error {
if p.Email == "" {
return badRequestError(ErrorCodeValidationFailed, "Password recovery requires an email")
}
var err error
if p.Email, err = validateEmail(p.Email); err != nil {
if p.Email, err = a.validateEmail(p.Email); err != nil {
return err
}
if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil {
Expand All @@ -38,7 +38,7 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error {
}

flowType := getFlowFromChallenge(params.CodeChallenge)
if err := params.Validate(); err != nil {
if err := params.Validate(a); err != nil {
return err
}

Expand Down
10 changes: 5 additions & 5 deletions internal/api/resend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"net/http"

"github.com/supabase/auth/internal/api/sms_provider"
"github.com/supabase/auth/internal/conf"
mail "github.com/supabase/auth/internal/mailer"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
Expand All @@ -17,7 +16,9 @@ type ResendConfirmationParams struct {
Phone string `json:"phone"`
}

func (p *ResendConfirmationParams) Validate(config *conf.GlobalConfiguration) error {
func (p *ResendConfirmationParams) Validate(a *API) error {
config := a.config

switch p.Type {
case mail.SignupVerification, mail.EmailChangeVerification, smsVerification, phoneChangeVerification:
break
Expand All @@ -40,7 +41,7 @@ func (p *ResendConfirmationParams) Validate(config *conf.GlobalConfiguration) er
if !config.External.Email.Enabled {
return badRequestError(ErrorCodeEmailProviderDisabled, "Email logins are disabled")
}
p.Email, err = validateEmail(p.Email)
p.Email, err = a.validateEmail(p.Email)
if err != nil {
return err
}
Expand All @@ -63,13 +64,12 @@ func (p *ResendConfirmationParams) Validate(config *conf.GlobalConfiguration) er
func (a *API) Resend(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config
params := &ResendConfirmationParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}

if err := params.Validate(config); err != nil {
if err := params.Validate(a); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
if !config.External.Email.Enabled {
return badRequestError(ErrorCodeEmailProviderDisabled, "Email signups are disabled")
}
params.Email, err = validateEmail(params.Email)
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (a *API) validateUserUpdateParams(ctx context.Context, p *UserUpdateParams)

var err error
if p.Email != "" {
p.Email, err = validateEmail(p.Email)
p.Email, err = a.validateEmail(p.Email)
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions internal/api/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type VerifyParams struct {
RedirectTo string `json:"redirect_to"`
}

func (p *VerifyParams) Validate(r *http.Request) error {
func (p *VerifyParams) Validate(r *http.Request, a *API) error {
var err error
if p.Type == "" {
return badRequestError(ErrorCodeValidationFailed, "Verify requires a verification type")
Expand All @@ -69,7 +69,7 @@ func (p *VerifyParams) Validate(r *http.Request) error {
}
p.TokenHash = crypto.GenerateTokenHash(p.Phone, p.Token)
} else if isEmailOtpVerification(p) {
p.Email, err = validateEmail(p.Email)
p.Email, err = a.validateEmail(p.Email)
if err != nil {
return unprocessableEntityError(ErrorCodeValidationFailed, "Invalid email format").WithInternalError(err)
}
Expand All @@ -96,15 +96,15 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error {
params.Token = r.FormValue("token")
params.Type = r.FormValue("type")
params.RedirectTo = utilities.GetReferrer(r, a.config)
if err := params.Validate(r); err != nil {
if err := params.Validate(r, a); err != nil {
return err
}
return a.verifyGet(w, r, params)
case http.MethodPost:
if err := retrieveRequestParams(r, params); err != nil {
return err
}
if err := params.Validate(r); err != nil {
if err := params.Validate(r, a); err != nil {
return err
}
return a.verifyPost(w, r, params)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ func (ts *VerifyTestSuite) TestVerifyValidateParams() {
for _, c := range cases {
ts.Run(c.desc, func() {
req := httptest.NewRequest(c.method, "http://localhost", nil)
err := c.params.Validate(req)
err := c.params.Validate(req, ts.API)
require.Equal(ts.T(), c.expected, err)
})
}
Expand Down
2 changes: 2 additions & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ type AnonymousProviderConfiguration struct {
type EmailProviderConfiguration struct {
Enabled bool `json:"enabled" default:"true"`

AuthorizedAddresses []string `json:"authorized_addresses" split_words:"true"`

MagicLinkEnabled bool `json:"magic_link_enabled" default:"true" split_words:"true"`
}

Expand Down

0 comments on commit 738e079

Please sign in to comment.