Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,10 @@ Email subject to use for email change confirmation. Defaults to `Confirm Email C

Email subject to use for password changed notification. Defaults to `Your password has been changed`.

`MAILER_SUBJECTS_EMAIL_CHANGED_NOTIFICATION` - `string`

Email subject to use for email changed notification. Defaults to `Your email address has been changed`.

`MAILER_TEMPLATES_INVITE` - `string`

URL path to an email template to use when inviting a user. (e.g. `https://www.example.com/path-to-email-template.html`)
Expand Down Expand Up @@ -667,7 +671,7 @@ Default Content (if template is unavailable):
`MAILER_TEMPLATES_PASSWORD_CHANGED_NOTIFICATION` - `string`

URL path to an email template to use when notifying a user that their password has been changed. (e.g. `https://www.example.com/path-to-email-template.html`)
`SiteURL` and `Email` variables are available.
`Email` variables are available.

Default Content (if template is unavailable):

Expand All @@ -679,12 +683,34 @@ Default Content (if template is unavailable):
just been changed. If you did not make this change, please contact support
immediately.
</p>
<p>If you did not make this change, please contact support.</p>
```

`MAILER_NOTIFICATION_CONFIGURATIONS_PASSWORD_CHANGED_NOTIFICATION_ENABLED` - `bool`
`GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED` - `bool`

Whether to send a notification email when a user's password is changed. Defaults to `false`.

`MAILER_TEMPLATES_EMAIL_CHANGED_NOTIFICATION` - `string`

URL path to an email template to use when notifying a user that their email has been changed. (e.g. `https://www.example.com/path-to-email-template.html`)
`Email` and `OldEmail` variables are available.

Default Content (if template is unavailable):

```html
<h2>Your email address has been changed</h2>

<p>
The email address for your account has been changed from {{ .OldEmail }} to {{
.Email }}.
</p>
<p>If you did not make this change, please contact support.</p>
```

`GOTRUE_MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED` - `bool`

Whether to send a notification email when a user's email is changed. Defaults to `false`.

### Phone Auth

`SMS_AUTOCONFIRM` - `bool`
Expand Down
3 changes: 3 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ GOTRUE_MAILER_SUBJECTS_MAGIC_LINK="Your Magic Link"
GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGE="Confirm Email Change"
GOTRUE_MAILER_SUBJECTS_INVITE="You have been invited"
GOTRUE_MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION="Your password has been changed"
GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGED_NOTIFICATION="Your email address has been changed"
GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED="true"

# Custom mailer template config
Expand All @@ -45,9 +46,11 @@ GOTRUE_MAILER_TEMPLATES_RECOVERY=""
GOTRUE_MAILER_TEMPLATES_MAGIC_LINK=""
GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE=""
GOTRUE_MAILER_TEMPLATES_PASSWORD_CHANGED_NOTIFICATION=""
GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGED_NOTIFICATION=""

# Account changes notifications configuration
GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED="false"
GOTRUE_MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED="false"

# Signup config
GOTRUE_DISABLE_SIGNUP="false"
Expand Down
96 changes: 78 additions & 18 deletions internal/api/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,11 @@ func (a *API) sendConfirmation(r *http.Request, tx *storage.Connection, u *model
token := crypto.GenerateTokenHash(u.GetEmail(), otp)
u.ConfirmationToken = addFlowPrefixToToken(token, flowType)
now := time.Now()
if err = a.sendEmail(r, tx, u, mail.SignupVerification, otp, "", u.ConfirmationToken); err != nil {
if err = a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.SignupVerification,
otp: otp,
tokenHashWithPrefix: u.ConfirmationToken,
}); err != nil {
u.ConfirmationToken = oldToken
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
Expand Down Expand Up @@ -358,7 +362,12 @@ func (a *API) sendInvite(r *http.Request, tx *storage.Connection, u *models.User

u.ConfirmationToken = crypto.GenerateTokenHash(u.GetEmail(), otp)
now := time.Now()
if err = a.sendEmail(r, tx, u, mail.InviteVerification, otp, "", u.ConfirmationToken); err != nil {
err = a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.InviteVerification,
otp: otp,
tokenHashWithPrefix: u.ConfirmationToken,
})
if err != nil {
u.ConfirmationToken = oldToken
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
Expand Down Expand Up @@ -396,7 +405,12 @@ func (a *API) sendPasswordRecovery(r *http.Request, tx *storage.Connection, u *m
token := crypto.GenerateTokenHash(u.GetEmail(), otp)
u.RecoveryToken = addFlowPrefixToToken(token, flowType)
now := time.Now()
if err := a.sendEmail(r, tx, u, mail.RecoveryVerification, otp, "", u.RecoveryToken); err != nil {
err := a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.RecoveryVerification,
otp: otp,
tokenHashWithPrefix: u.RecoveryToken,
})
if err != nil {
u.RecoveryToken = oldToken
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
Expand Down Expand Up @@ -433,7 +447,12 @@ func (a *API) sendReauthenticationOtp(r *http.Request, tx *storage.Connection, u
u.ReauthenticationToken = crypto.GenerateTokenHash(u.GetEmail(), otp)
now := time.Now()

if err := a.sendEmail(r, tx, u, mail.ReauthenticationVerification, otp, "", u.ReauthenticationToken); err != nil {
err := a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.ReauthenticationVerification,
otp: otp,
tokenHashWithPrefix: u.ReauthenticationToken,
})
if err != nil {
u.ReauthenticationToken = oldToken
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
Expand Down Expand Up @@ -472,7 +491,11 @@ func (a *API) sendMagicLink(r *http.Request, tx *storage.Connection, u *models.U
u.RecoveryToken = addFlowPrefixToToken(token, flowType)

now := time.Now()
if err = a.sendEmail(r, tx, u, mail.MagicLinkVerification, otp, "", u.RecoveryToken); err != nil {
if err = a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.MagicLinkVerification,
otp: otp,
tokenHashWithPrefix: u.RecoveryToken,
}); err != nil {
u.RecoveryToken = oldToken
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
Expand Down Expand Up @@ -519,7 +542,13 @@ func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models
u.EmailChangeConfirmStatus = zeroConfirmation
now := time.Now()

if err := a.sendEmail(r, tx, u, mail.EmailChangeVerification, otpCurrent, otpNew, u.EmailChangeTokenNew); err != nil {
err := a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.EmailChangeVerification,
otp: otpCurrent,
otpNew: otpNew,
tokenHashWithPrefix: u.EmailChangeTokenNew,
})
if err != nil {
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
} else if herr, ok := err.(*HTTPError); ok {
Expand Down Expand Up @@ -556,7 +585,10 @@ func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models
}

func (a *API) sendPasswordChangedNotification(r *http.Request, tx *storage.Connection, u *models.User) error {
if err := a.sendEmail(r, tx, u, mail.PasswordChangedNotification, "", "", ""); err != nil {
err := a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.PasswordChangedNotification,
})
if err != nil {
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
} else if herr, ok := err.(*HTTPError); ok {
Expand All @@ -568,6 +600,23 @@ func (a *API) sendPasswordChangedNotification(r *http.Request, tx *storage.Conne
return nil
}

func (a *API) sendEmailChangedNotification(r *http.Request, tx *storage.Connection, u *models.User, oldEmail string) error {
err := a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.EmailChangedNotification,
oldEmail: oldEmail,
})
if err != nil {
if errors.Is(err, EmailRateLimitExceeded) {
return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
} else if herr, ok := err.(*HTTPError); ok {
return herr
}
return apierrors.NewInternalServerError("Error sending email changed notification email").WithInternalError(err)
}

return nil
}

func (a *API) validateEmail(email string) (string, error) {
if email == "" {
return "", apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "An email address is required")
Expand Down Expand Up @@ -608,13 +657,22 @@ func (a *API) checkEmailAddressAuthorization(email string) bool {
return true
}

func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User, emailActionType, otp, otpNew, tokenHashWithPrefix string) error {
type sendEmailParams struct {
emailActionType string
otp string
otpNew string
tokenHashWithPrefix string
oldEmail string
}

func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User, params sendEmailParams) error {
ctx := r.Context()
config := a.config
referrerURL := utilities.GetReferrer(r, config)
externalURL := getExternalHost(ctx)
otp := params.otp

if emailActionType != mail.EmailChangeVerification {
if params.emailActionType != mail.EmailChangeVerification {
if u.GetEmail() != "" && !a.checkEmailAddressAuthorization(u.GetEmail()) {
return apierrors.NewBadRequestError(apierrors.ErrorCodeEmailAddressNotAuthorized, "Email address %q cannot be used as it is not authorized", u.GetEmail())
}
Expand Down Expand Up @@ -659,7 +717,7 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User,

if config.Hook.SendEmail.Enabled {
// When secure email change is disabled, we place the token for the new email on emailData.Token
if emailActionType == mail.EmailChangeVerification && !config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" {
if params.emailActionType == mail.EmailChangeVerification && !config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" {

// BUG(cstockton): This introduced a bug which mismatched the token
// and hash fields, such that:
Expand All @@ -676,26 +734,26 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User,
// Token Always contains the Token for user.email_new
// TokenHash Always contains the Hash for user.email_new
//
otp = otpNew
otp = params.otpNew
}

emailData := mail.EmailData{
Token: otp,
EmailActionType: emailActionType,
EmailActionType: params.emailActionType,
RedirectTo: referrerURL,
SiteURL: externalURL.String(),
TokenHash: tokenHashWithPrefix,
TokenHash: params.tokenHashWithPrefix,
}
if emailActionType == mail.EmailChangeVerification {
if params.emailActionType == mail.EmailChangeVerification {
if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" {
emailData.TokenNew = otpNew
emailData.TokenNew = params.otpNew
emailData.TokenHashNew = u.EmailChangeTokenCurrent
} else if emailData.Token == "" && u.EmailChange != "" {

// BUG(cstockton): This matches the current behavior but is not
// intuitive and should be changed in a future release. See the
// comment above for more details.
emailData.Token = otpNew
emailData.Token = params.otpNew
}
}
input := v0hooks.SendEmailInput{
Expand All @@ -708,7 +766,7 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User,

mr := a.Mailer()
var err error
switch emailActionType {
switch params.emailActionType {
case mail.SignupVerification:
err = mr.ConfirmationMail(r, u, otp, referrerURL, externalURL)
case mail.MagicLinkVerification:
Expand All @@ -720,9 +778,11 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User,
case mail.InviteVerification:
err = mr.InviteMail(r, u, otp, referrerURL, externalURL)
case mail.EmailChangeVerification:
err = mr.EmailChangeMail(r, u, otpNew, otp, referrerURL, externalURL)
err = mr.EmailChangeMail(r, u, params.otpNew, otp, referrerURL, externalURL)
case mail.PasswordChangedNotification:
err = mr.PasswordChangedNotificationMail(r, u)
case mail.EmailChangedNotification:
err = mr.EmailChangedNotificationMail(r, u, params.oldEmail)
default:
err = errors.New("invalid email action type")
}
Expand Down
10 changes: 10 additions & 0 deletions internal/api/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/fatih/structs"
"github.com/sethvargo/go-password/password"
"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/api/apierrors"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/api/sms_provider"
Expand Down Expand Up @@ -559,6 +560,7 @@ func (a *API) emailChangeVerify(r *http.Request, conn *storage.Connection, param
}

// one email is confirmed at this point if GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED is enabled
oldEmail := user.GetEmail()
err := conn.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.UserModifiedAction, "", nil); terr != nil {
return terr
Expand Down Expand Up @@ -603,6 +605,14 @@ func (a *API) emailChangeVerify(r *http.Request, conn *storage.Connection, param
return nil, err
}

// send an Email Changed email notification to the user
if config.Mailer.Notifications.EmailChangedEnabled && user.GetEmail() != "" {
if err := a.sendEmailChangedNotification(r, conn, user, oldEmail); err != nil {
// we don't want to fail the whole request if the email can't be sent
logrus.WithError(err).Warn("Unable to send email changed notification")
}
}

return user, nil
}

Expand Down
Loading