267 lines
8.3 KiB
Go
267 lines
8.3 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/supabase/auth/internal/api/sms_provider"
|
|
"github.com/supabase/auth/internal/mailer"
|
|
"github.com/supabase/auth/internal/models"
|
|
"github.com/supabase/auth/internal/storage"
|
|
)
|
|
|
|
// UserUpdateParams parameters for updating a user
|
|
type UserUpdateParams struct {
|
|
Email string `json:"email"`
|
|
Password *string `json:"password"`
|
|
Nonce string `json:"nonce"`
|
|
Data map[string]interface{} `json:"data"`
|
|
AppData map[string]interface{} `json:"app_metadata,omitempty"`
|
|
Phone string `json:"phone"`
|
|
Channel string `json:"channel"`
|
|
CodeChallenge string `json:"code_challenge"`
|
|
CodeChallengeMethod string `json:"code_challenge_method"`
|
|
}
|
|
|
|
func (a *API) validateUserUpdateParams(ctx context.Context, p *UserUpdateParams) error {
|
|
config := a.config
|
|
|
|
var err error
|
|
if p.Email != "" {
|
|
p.Email, err = a.validateEmail(p.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if p.Phone != "" {
|
|
if p.Phone, err = validatePhone(p.Phone); err != nil {
|
|
return err
|
|
}
|
|
if p.Channel == "" {
|
|
p.Channel = sms_provider.SMSProvider
|
|
}
|
|
if !sms_provider.IsValidMessageChannel(p.Channel, config) {
|
|
return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
|
|
}
|
|
}
|
|
|
|
if p.Password != nil {
|
|
if err := a.checkPasswordStrength(ctx, *p.Password); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UserGet returns a user
|
|
func (a *API) UserGet(w http.ResponseWriter, r *http.Request) error {
|
|
ctx := r.Context()
|
|
claims := getClaims(ctx)
|
|
if claims == nil {
|
|
return internalServerError("Could not read claims")
|
|
}
|
|
|
|
aud := a.requestAud(ctx, r)
|
|
audienceFromClaims, _ := claims.GetAudience()
|
|
if len(audienceFromClaims) == 0 || aud != audienceFromClaims[0] {
|
|
return badRequestError(ErrorCodeValidationFailed, "Token audience doesn't match request audience")
|
|
}
|
|
|
|
user := getUser(ctx)
|
|
return sendJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
// UserUpdate updates fields on a user
|
|
func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error {
|
|
ctx := r.Context()
|
|
db := a.db.WithContext(ctx)
|
|
config := a.config
|
|
aud := a.requestAud(ctx, r)
|
|
|
|
params := &UserUpdateParams{}
|
|
if err := retrieveRequestParams(r, params); err != nil {
|
|
return err
|
|
}
|
|
|
|
user := getUser(ctx)
|
|
session := getSession(ctx)
|
|
|
|
if err := a.validateUserUpdateParams(ctx, params); err != nil {
|
|
return err
|
|
}
|
|
|
|
if params.AppData != nil && !isAdmin(user, config) {
|
|
if !isAdmin(user, config) {
|
|
return forbiddenError(ErrorCodeNotAdmin, "Updating app_metadata requires admin privileges")
|
|
}
|
|
}
|
|
|
|
if user.HasMFAEnabled() && !session.IsAAL2() {
|
|
if (params.Password != nil && *params.Password != "") || (params.Email != "" && user.GetEmail() != params.Email) || (params.Phone != "" && user.GetPhone() != params.Phone) {
|
|
return httpError(http.StatusUnauthorized, ErrorCodeInsufficientAAL, "AAL2 session is required to update email or password when MFA is enabled.")
|
|
}
|
|
}
|
|
|
|
if user.IsAnonymous {
|
|
if params.Password != nil && *params.Password != "" {
|
|
if params.Email == "" && params.Phone == "" {
|
|
return unprocessableEntityError(ErrorCodeValidationFailed, "Updating password of an anonymous user without an email or phone is not allowed")
|
|
}
|
|
}
|
|
}
|
|
|
|
if user.IsSSOUser {
|
|
updatingForbiddenFields := false
|
|
|
|
updatingForbiddenFields = updatingForbiddenFields || (params.Password != nil && *params.Password != "")
|
|
updatingForbiddenFields = updatingForbiddenFields || (params.Email != "" && params.Email != user.GetEmail())
|
|
updatingForbiddenFields = updatingForbiddenFields || (params.Phone != "" && params.Phone != user.GetPhone())
|
|
updatingForbiddenFields = updatingForbiddenFields || (params.Nonce != "")
|
|
|
|
if updatingForbiddenFields {
|
|
return unprocessableEntityError(ErrorCodeUserSSOManaged, "Updating email, phone, password of a SSO account only possible via SSO")
|
|
}
|
|
}
|
|
|
|
if params.Email != "" && user.GetEmail() != params.Email {
|
|
if duplicateUser, err := models.IsDuplicatedEmail(db, params.Email, aud, user); err != nil {
|
|
return internalServerError("Database error checking email").WithInternalError(err)
|
|
} else if duplicateUser != nil {
|
|
return unprocessableEntityError(ErrorCodeEmailExists, DuplicateEmailMsg)
|
|
}
|
|
}
|
|
|
|
if params.Phone != "" && user.GetPhone() != params.Phone {
|
|
if exists, err := models.IsDuplicatedPhone(db, params.Phone, aud); err != nil {
|
|
return internalServerError("Database error checking phone").WithInternalError(err)
|
|
} else if exists {
|
|
return unprocessableEntityError(ErrorCodePhoneExists, DuplicatePhoneMsg)
|
|
}
|
|
}
|
|
|
|
if params.Password != nil {
|
|
if config.Security.UpdatePasswordRequireReauthentication {
|
|
now := time.Now()
|
|
// we require reauthentication if the user hasn't signed in recently in the current session
|
|
if session == nil || now.After(session.CreatedAt.Add(24*time.Hour)) {
|
|
if len(params.Nonce) == 0 {
|
|
return badRequestError(ErrorCodeReauthenticationNeeded, "Password update requires reauthentication")
|
|
}
|
|
if err := a.verifyReauthentication(params.Nonce, db, config, user); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
password := *params.Password
|
|
if password != "" {
|
|
isSamePassword := false
|
|
|
|
if user.HasPassword() {
|
|
auth, _, err := user.Authenticate(ctx, db, password, config.Security.DBEncryption.DecryptionKeys, false, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isSamePassword = auth
|
|
}
|
|
|
|
if isSamePassword {
|
|
return unprocessableEntityError(ErrorCodeSamePassword, "New password should be different from the old password.")
|
|
}
|
|
}
|
|
|
|
if err := user.SetPassword(ctx, password, config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := db.Transaction(func(tx *storage.Connection) error {
|
|
var terr error
|
|
if params.Password != nil {
|
|
var sessionID *uuid.UUID
|
|
if session != nil {
|
|
sessionID = &session.ID
|
|
}
|
|
|
|
if terr = user.UpdatePassword(tx, sessionID); terr != nil {
|
|
return internalServerError("Error during password storage").WithInternalError(terr)
|
|
}
|
|
|
|
if terr := models.NewAuditLogEntry(r, tx, user, models.UserUpdatePasswordAction, "", nil); terr != nil {
|
|
return terr
|
|
}
|
|
}
|
|
|
|
if params.Data != nil {
|
|
if terr = user.UpdateUserMetaData(tx, params.Data); terr != nil {
|
|
return internalServerError("Error updating user").WithInternalError(terr)
|
|
}
|
|
}
|
|
|
|
if params.AppData != nil {
|
|
if terr = user.UpdateAppMetaData(tx, params.AppData); terr != nil {
|
|
return internalServerError("Error updating user").WithInternalError(terr)
|
|
}
|
|
}
|
|
|
|
if params.Email != "" && params.Email != user.GetEmail() {
|
|
if user.IsAnonymous && config.Mailer.Autoconfirm {
|
|
// anonymous users can add an email with automatic confirmation, which is similar to signing up
|
|
// permanent users always need to verify their email address when changing it
|
|
user.EmailChange = params.Email
|
|
if _, terr := a.emailChangeVerify(r, tx, &VerifyParams{
|
|
Type: mailer.EmailChangeVerification,
|
|
Email: params.Email,
|
|
}, user); terr != nil {
|
|
return terr
|
|
}
|
|
|
|
} else {
|
|
flowType := getFlowFromChallenge(params.CodeChallenge)
|
|
if isPKCEFlow(flowType) {
|
|
_, terr := generateFlowState(tx, models.EmailChange.String(), models.EmailChange, params.CodeChallengeMethod, params.CodeChallenge, &user.ID)
|
|
if terr != nil {
|
|
return terr
|
|
}
|
|
|
|
}
|
|
if terr = a.sendEmailChange(r, tx, user, params.Email, flowType); terr != nil {
|
|
return terr
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.Phone != "" && params.Phone != user.GetPhone() {
|
|
if config.Sms.Autoconfirm {
|
|
user.PhoneChange = params.Phone
|
|
if _, terr := a.smsVerify(r, tx, user, &VerifyParams{
|
|
Type: phoneChangeVerification,
|
|
Phone: params.Phone,
|
|
}); terr != nil {
|
|
return terr
|
|
}
|
|
} else {
|
|
if _, terr := a.sendPhoneConfirmation(r, tx, user, params.Phone, phoneChangeVerification, params.Channel); terr != nil {
|
|
return terr
|
|
}
|
|
}
|
|
}
|
|
|
|
if terr = models.NewAuditLogEntry(r, tx, user, models.UserModifiedAction, "", nil); terr != nil {
|
|
return internalServerError("Error recording audit log entry").WithInternalError(terr)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return sendJSON(w, http.StatusOK, user)
|
|
}
|