chatdesk-ui/auth_v2.169.0/internal/api/user.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)
}