Skip to content
Open
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
1 change: 1 addition & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ GOTRUE_LOG_LEVEL="debug"
GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED="false"
GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL="0"
GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION="false"
GOTRUE_SECURITY_OTP_TOKEN_HASH_SALT=""
GOTRUE_OPERATOR_TOKEN="unused-operator-token"

# Trusted rate limiting header - this should be set by a trusted upstream proxy
Expand Down
30 changes: 15 additions & 15 deletions internal/api/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func signupAndConfirmEmail(
require.False(t, hookReq.Metadata.Time.IsZero())

// verify that the latest user from find user matches OTP
otpHash := crypto.GenerateTokenHash(
otpHash := crypto.GenerateTokenHash("",
expUser.GetEmail(), hookReq.EmailData.Token)
require.NotEmpty(t, hookReq.EmailData.Token)
require.Equal(t, otpHash, hookReq.EmailData.TokenHash)
Expand All @@ -208,7 +208,7 @@ func signupAndConfirmEmail(
// otp matches the user id and email
ott, err := models.FindOneTimeToken(
inst.Conn,
hookReq.EmailData.TokenHash,
[]string{hookReq.EmailData.TokenHash},
models.ConfirmationToken)
require.NoError(t, err)
require.Equal(t, expUser.ID.String(), ott.UserID.String())
Expand Down Expand Up @@ -312,12 +312,12 @@ func TestE2EHooks(t *testing.T) {
require.NotNil(t, latestUser)

otp := hookReq.SMS.OTP
otpHash := crypto.GenerateTokenHash(
otpHash := crypto.GenerateTokenHash("",
signupUser.GetPhone(), hookReq.SMS.OTP)

ott, err := models.FindOneTimeToken(
inst.Conn,
otpHash,
[]string{otpHash},
models.ConfirmationToken)
require.NoError(t, err)
require.Equal(t, signupUser.ID.String(), ott.UserID.String())
Expand Down Expand Up @@ -417,12 +417,12 @@ func TestE2EHooks(t *testing.T) {
require.Equal(t, currentUser.AppMetaData, hookReq.User.AppMetaData)

otp = hookReq.SMS.OTP
otpHash := crypto.GenerateTokenHash(
otpHash := crypto.GenerateTokenHash("",
currentUser.PhoneChange, hookReq.SMS.OTP)

ott, err := models.FindOneTimeToken(
inst.Conn,
otpHash,
[]string{otpHash},
models.PhoneChangeToken)
require.NoError(t, err)
require.Equal(t, currentUser.ID.String(), ott.UserID.String())
Expand Down Expand Up @@ -1184,9 +1184,9 @@ func TestE2EHooks(t *testing.T) {
require.Equal(t, newEmail, hookReq.User.EmailChange)

// verify otps
curOtpHash := crypto.GenerateTokenHash(
curOtpHash := crypto.GenerateTokenHash("",
curEmail, hookReq.EmailData.Token)
newOtpHash := crypto.GenerateTokenHash(
newOtpHash := crypto.GenerateTokenHash("",
newEmail, hookReq.EmailData.TokenNew)

// The hashes are switched incorrectly in the current code, i.e.:
Expand All @@ -1208,7 +1208,7 @@ func TestE2EHooks(t *testing.T) {
// verify there is an ott generated
ott, err := models.FindOneTimeToken(
inst.Conn,
hookReq.EmailData.TokenHash,
[]string{hookReq.EmailData.TokenHash},
models.EmailChangeTokenNew)
require.NoError(t, err)
require.Equal(t, signupUser.ID.String(), ott.UserID.String())
Expand Down Expand Up @@ -1290,7 +1290,7 @@ func TestE2EHooks(t *testing.T) {
// verify there is an ott generated
ott, err := models.FindOneTimeToken(
inst.Conn,
hookReq.EmailData.TokenHash,
[]string{hookReq.EmailData.TokenHash},
models.EmailChangeTokenNew)
require.NoError(t, err)
require.Equal(t, signupUser.ID.String(), ott.UserID.String())
Expand All @@ -1313,7 +1313,7 @@ func TestE2EHooks(t *testing.T) {
require.Empty(t, hookReq.EmailData.TokenHashNew)

// verify otps
newOtpHash := crypto.GenerateTokenHash(
newOtpHash := crypto.GenerateTokenHash("",
newEmail, hookReq.EmailData.Token)

// The new email is stored on fields without _new suffix.
Expand Down Expand Up @@ -1413,7 +1413,7 @@ func TestE2EHooks(t *testing.T) {
require.Equal(t, newEmail, hookReq.User.EmailChange)

// verify otps
newOtpHash := crypto.GenerateTokenHash(
newOtpHash := crypto.GenerateTokenHash("",
newEmail, hookReq.EmailData.Token)

// The new email is stored on fields without _new suffix.
Expand All @@ -1433,7 +1433,7 @@ func TestE2EHooks(t *testing.T) {
// verify there is an ott generated
ott, err := models.FindOneTimeToken(
inst.Conn,
hookReq.EmailData.TokenHash,
[]string{hookReq.EmailData.TokenHash},
models.EmailChangeTokenNew)
require.NoError(t, err)
require.Equal(t, signupUser.ID.String(), ott.UserID.String())
Expand Down Expand Up @@ -1515,7 +1515,7 @@ func TestE2EHooks(t *testing.T) {
// verify there is an ott generated
ott, err := models.FindOneTimeToken(
inst.Conn,
hookReq.EmailData.TokenHash,
[]string{hookReq.EmailData.TokenHash},
models.EmailChangeTokenNew)
require.NoError(t, err)
require.Equal(t, signupUser.ID.String(), ott.UserID.String())
Expand All @@ -1538,7 +1538,7 @@ func TestE2EHooks(t *testing.T) {
require.Empty(t, hookReq.EmailData.TokenHashNew)

// verify otps
newOtpHash := crypto.GenerateTokenHash(
newOtpHash := crypto.GenerateTokenHash("",
newEmail, hookReq.EmailData.Token)

// The new email is stored on fields without _new suffix.
Expand Down
2 changes: 1 addition & 1 deletion internal/api/invite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func (ts *InviteTestSuite) TestVerifyInvite() {
user.InvitedAt = &now
user.ConfirmationSentAt = &now
user.EncryptedPassword = nil
user.ConfirmationToken = crypto.GenerateTokenHash(c.email, c.requestBody["token"].(string))
user.ConfirmationToken = crypto.GenerateTokenHash("", c.email, c.requestBody["token"].(string))
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(user))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, user.ID, user.GetEmail(), user.ConfirmationToken, models.ConfirmationToken))
Expand Down
18 changes: 9 additions & 9 deletions internal/api/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
now := time.Now()
otp := crypto.GenerateOtp(config.Mailer.OtpLength)

hashedToken := crypto.GenerateTokenHash(params.Email, otp)
hashedToken := crypto.GenerateTokenHash(config.Security.TokenHashSalt, params.Email, otp)

var (
createdUser bool
Expand Down Expand Up @@ -258,7 +258,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
if params.Type == "email_change_current" {
user.EmailChangeTokenCurrent = hashedToken
} else if params.Type == "email_change_new" {
user.EmailChangeTokenNew = crypto.GenerateTokenHash(params.NewEmail, otp)
user.EmailChangeTokenNew = crypto.GenerateTokenHash(config.Security.TokenHashSalt, params.NewEmail, otp)
}
terr = tx.UpdateOnly(user, "email_change_token_current", "email_change_token_new", "email_change", "email_change_sent_at", "email_change_confirm_status")
if terr != nil {
Expand Down Expand Up @@ -328,7 +328,7 @@ func (a *API) sendConfirmation(r *http.Request, tx *storage.Connection, u *model
oldToken := u.ConfirmationToken
otp := crypto.GenerateOtp(otpLength)

token := crypto.GenerateTokenHash(u.GetEmail(), otp)
token := crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.GetEmail(), otp)
u.ConfirmationToken = addFlowPrefixToToken(token, flowType)
now := time.Now()
if err = a.sendEmail(r, tx, u, sendEmailParams{
Expand Down Expand Up @@ -363,7 +363,7 @@ func (a *API) sendInvite(r *http.Request, tx *storage.Connection, u *models.User
oldToken := u.ConfirmationToken
otp := crypto.GenerateOtp(otpLength)

u.ConfirmationToken = crypto.GenerateTokenHash(u.GetEmail(), otp)
u.ConfirmationToken = crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.GetEmail(), otp)
now := time.Now()
err = a.sendEmail(r, tx, u, sendEmailParams{
emailActionType: mail.InviteVerification,
Expand Down Expand Up @@ -405,7 +405,7 @@ func (a *API) sendPasswordRecovery(r *http.Request, tx *storage.Connection, u *m
oldToken := u.RecoveryToken
otp := crypto.GenerateOtp(otpLength)

token := crypto.GenerateTokenHash(u.GetEmail(), otp)
token := crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.GetEmail(), otp)
u.RecoveryToken = addFlowPrefixToToken(token, flowType)
now := time.Now()
err := a.sendEmail(r, tx, u, sendEmailParams{
Expand Down Expand Up @@ -447,7 +447,7 @@ func (a *API) sendReauthenticationOtp(r *http.Request, tx *storage.Connection, u
oldToken := u.ReauthenticationToken
otp := crypto.GenerateOtp(otpLength)

u.ReauthenticationToken = crypto.GenerateTokenHash(u.GetEmail(), otp)
u.ReauthenticationToken = crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.GetEmail(), otp)
now := time.Now()

err := a.sendEmail(r, tx, u, sendEmailParams{
Expand Down Expand Up @@ -490,7 +490,7 @@ func (a *API) sendMagicLink(r *http.Request, tx *storage.Connection, u *models.U
oldToken := u.RecoveryToken
otp := crypto.GenerateOtp(otpLength)

token := crypto.GenerateTokenHash(u.GetEmail(), otp)
token := crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.GetEmail(), otp)
u.RecoveryToken = addFlowPrefixToToken(token, flowType)

now := time.Now()
Expand Down Expand Up @@ -531,14 +531,14 @@ func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models
otpNew := crypto.GenerateOtp(otpLength)

u.EmailChange = email
token := crypto.GenerateTokenHash(u.EmailChange, otpNew)
token := crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.EmailChange, otpNew)
u.EmailChangeTokenNew = addFlowPrefixToToken(token, flowType)

otpCurrent := ""
if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" {
otpCurrent = crypto.GenerateOtp(otpLength)

currentToken := crypto.GenerateTokenHash(u.GetEmail(), otpCurrent)
currentToken := crypto.GenerateTokenHash(config.Security.TokenHashSalt, u.GetEmail(), otpCurrent)
u.EmailChangeTokenCurrent = addFlowPrefixToToken(currentToken, flowType)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/mail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func (ts *MailTestSuite) TestGenerateLink() {
require.Equal(ts.T(), c.ExpectedResponse["redirect_to"], data["redirect_to"])

// check if hashed_token matches hash function of email and the raw otp
require.Equal(ts.T(), crypto.GenerateTokenHash(c.Body.Email, data["email_otp"].(string)), data["hashed_token"])
require.Equal(ts.T(), crypto.GenerateTokenHash("", c.Body.Email, data["email_otp"].(string)), data["hashed_token"])

// check if the host used in the email link matches the initial request host
u, err := url.ParseRequestURI(data["action_link"].(string))
Expand Down
12 changes: 6 additions & 6 deletions internal/api/oauthserver/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,12 +374,12 @@ func (s *Server) regenerateOAuthServerClientSecret(ctx context.Context, clientID

// OAuthServerClientUpdateParams contains parameters for updating an OAuth client
type OAuthServerClientUpdateParams struct {
RedirectURIs *[]string `json:"redirect_uris,omitempty"`
GrantTypes *[]string `json:"grant_types,omitempty"`
ClientName *string `json:"client_name,omitempty"`
ClientURI *string `json:"client_uri,omitempty"`
LogoURI *string `json:"logo_uri,omitempty"`
TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"`
RedirectURIs *[]string `json:"redirect_uris,omitempty"`
GrantTypes *[]string `json:"grant_types,omitempty"`
ClientName *string `json:"client_name,omitempty"`
ClientURI *string `json:"client_uri,omitempty"`
LogoURI *string `json:"logo_uri,omitempty"`
TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"`
}

// isEmpty returns true if no fields are set for update
Expand Down
2 changes: 1 addition & 1 deletion internal/api/phone.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (a *API) sendPhoneConfirmation(r *http.Request, tx *storage.Connection, use
}
}

*token = crypto.GenerateTokenHash(phone, otp)
*token = crypto.GenerateTokenHash(config.Security.TokenHashSalt, phone, otp)

switch otpType {
case phoneConfirmationOtp:
Expand Down
4 changes: 2 additions & 2 deletions internal/api/provider/custom_oauth_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func TestCustomOIDCProvider_GetUserData_UserinfoAllowlist(t *testing.T) {
provider, err := NewCustomOIDCProvider(
context.Background(),
"client-id", "client-secret", "https://myapp.com/callback",
[]string{"openid"}, server.URL, server.URL + "/.well-known/openid-configuration", false,
[]string{"openid"}, server.URL, server.URL+"/.well-known/openid-configuration", false,
nil, nil, nil,
[]string{"mail", "sn"},
newTestOIDCProviderCache(t, 0),
Expand Down Expand Up @@ -257,7 +257,7 @@ func TestCustomOIDCProvider_GetUserData_IDTokenAllowlist(t *testing.T) {
provider, err := NewCustomOIDCProvider(
context.Background(),
"client-id", "client-secret", "https://myapp.com/callback",
[]string{"openid"}, server.URL, server.URL + "/.well-known/openid-configuration", false,
[]string{"openid"}, server.URL, server.URL+"/.well-known/openid-configuration", false,
nil, nil, nil,
[]string{"groups", "org_id"},
newTestOIDCProviderCache(t, 0),
Expand Down
34 changes: 17 additions & 17 deletions internal/api/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,24 @@ type Claims struct {
Exp float64 `json:"exp,omitempty" structs:"exp,omitempty"`

// Default profile claims
Name string `json:"name,omitempty" structs:"name,omitempty"`
FamilyName string `json:"family_name,omitempty" structs:"family_name,omitempty"`
GivenName string `json:"given_name,omitempty" structs:"given_name,omitempty"`
MiddleName string `json:"middle_name,omitempty" structs:"middle_name,omitempty"`
NickName string `json:"nickname,omitempty" structs:"nickname,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty" structs:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty" structs:"profile,omitempty"`
Picture string `json:"picture,omitempty" structs:"picture,omitempty"`
Website string `json:"website,omitempty" structs:"website,omitempty"`
Gender string `json:"gender,omitempty" structs:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty" structs:"birthdate,omitempty"`
ZoneInfo string `json:"zoneinfo,omitempty" structs:"zoneinfo,omitempty"`
Locale string `json:"locale,omitempty" structs:"locale,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
FamilyName string `json:"family_name,omitempty" structs:"family_name,omitempty"`
GivenName string `json:"given_name,omitempty" structs:"given_name,omitempty"`
MiddleName string `json:"middle_name,omitempty" structs:"middle_name,omitempty"`
NickName string `json:"nickname,omitempty" structs:"nickname,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty" structs:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty" structs:"profile,omitempty"`
Picture string `json:"picture,omitempty" structs:"picture,omitempty"`
Website string `json:"website,omitempty" structs:"website,omitempty"`
Gender string `json:"gender,omitempty" structs:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty" structs:"birthdate,omitempty"`
ZoneInfo string `json:"zoneinfo,omitempty" structs:"zoneinfo,omitempty"`
Locale string `json:"locale,omitempty" structs:"locale,omitempty"`
UpdatedAt *UnixTimeOrString `json:"updated_at,omitempty" structs:"updated_at,omitempty"`
Email string `json:"email,omitempty" structs:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty" structs:"email_verified"`
Phone string `json:"phone,omitempty" structs:"phone,omitempty"`
PhoneVerified bool `json:"phone_verified,omitempty" structs:"phone_verified"`
Email string `json:"email,omitempty" structs:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty" structs:"email_verified"`
Phone string `json:"phone,omitempty" structs:"phone,omitempty"`
PhoneVerified bool `json:"phone_verified,omitempty" structs:"phone_verified"`

// Custom profile claims that are provider specific
CustomClaims map[string]interface{} `json:"custom_claims,omitempty" structs:"custom_claims,omitempty"`
Expand Down
20 changes: 18 additions & 2 deletions internal/api/reauthenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"net/http"

"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/api/apierrors"
"github.com/supabase/auth/internal/api/sms_provider"
"github.com/supabase/auth/internal/conf"
Expand Down Expand Up @@ -72,9 +73,17 @@ func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, confi
return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeReauthenticationNotValid, InvalidNonceMessage)
}
var isValid bool
salt := config.Security.TokenHashSalt
if user.GetEmail() != "" {
tokenHash := crypto.GenerateTokenHash(user.GetEmail(), nonce)
tokenHash := crypto.GenerateTokenHash(salt, user.GetEmail(), nonce)
isValid = isOtpValid(tokenHash, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Mailer.OtpExp)
if !isValid && salt != "" {
legacyHash := crypto.GenerateTokenHash("", user.GetEmail(), nonce)
if isOtpValid(legacyHash, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Mailer.OtpExp) {
isValid = true
logrus.Info("reauthentication token verified using legacy (pre-salt) token hash fallback")
}
}
} else if user.GetPhone() != "" {
if config.Sms.IsTwilioVerifyProvider() {
smsProvider, _ := sms_provider.GetSmsProvider(*config)
Expand All @@ -83,8 +92,15 @@ func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, confi
}
return nil
} else {
tokenHash := crypto.GenerateTokenHash(user.GetPhone(), nonce)
tokenHash := crypto.GenerateTokenHash(salt, user.GetPhone(), nonce)
isValid = isOtpValid(tokenHash, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Sms.OtpExp)
if !isValid && salt != "" {
legacyHash := crypto.GenerateTokenHash("", user.GetPhone(), nonce)
if isOtpValid(legacyHash, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Sms.OtpExp) {
isValid = true
logrus.Info("reauthentication token verified using legacy (pre-salt) token hash fallback")
}
}
}
} else {
return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeReauthenticationNotValid, "Reauthentication requires an email or a phone number")
Expand Down
22 changes: 11 additions & 11 deletions internal/api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ type ProviderSettings struct {
}

type Settings struct {
ExternalProviders ProviderSettings `json:"external"`
DisableSignup bool `json:"disable_signup"`
MailerAutoconfirm bool `json:"mailer_autoconfirm"`
PhoneAutoconfirm bool `json:"phone_autoconfirm"`
SmsProvider string `json:"sms_provider"`
SAMLEnabled bool `json:"saml_enabled"`
SAMLPrivateKeyNextConfigured bool `json:"saml_private_key_next_configured"`
PasskeysEnabled bool `json:"passkeys_enabled"`
ExternalProviders ProviderSettings `json:"external"`
DisableSignup bool `json:"disable_signup"`
MailerAutoconfirm bool `json:"mailer_autoconfirm"`
PhoneAutoconfirm bool `json:"phone_autoconfirm"`
SmsProvider string `json:"sms_provider"`
SAMLEnabled bool `json:"saml_enabled"`
SAMLPrivateKeyNextConfigured bool `json:"saml_private_key_next_configured"`
PasskeysEnabled bool `json:"passkeys_enabled"`
}

func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -74,9 +74,9 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Phone: config.External.Phone.Enabled,
Zoom: config.External.Zoom.Enabled,
},
DisableSignup: config.DisableSignup,
MailerAutoconfirm: config.Mailer.Autoconfirm,
PhoneAutoconfirm: config.Sms.Autoconfirm,
DisableSignup: config.DisableSignup,
MailerAutoconfirm: config.Mailer.Autoconfirm,
PhoneAutoconfirm: config.Sms.Autoconfirm,
SmsProvider: config.Sms.Provider,
SAMLEnabled: config.SAML.Enabled,
SAMLPrivateKeyNextConfigured: config.SAML.CertificateNext != nil,
Expand Down
Loading
Loading