package api import ( "context" "net/http" "github.com/fatih/structs" "github.com/go-chi/chi/v5" "github.com/gofrs/uuid" "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" ) func (a *API) DeleteIdentity(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() claims := getClaims(ctx) if claims == nil { return internalServerError("Could not read claims") } identityID, err := uuid.FromString(chi.URLParam(r, "identity_id")) if err != nil { return notFoundError(ErrorCodeValidationFailed, "identity_id must be an UUID") } aud := a.requestAud(ctx, r) audienceFromClaims, _ := claims.GetAudience() if len(audienceFromClaims) == 0 || aud != audienceFromClaims[0] { return forbiddenError(ErrorCodeUnexpectedAudience, "Token audience doesn't match request audience") } user := getUser(ctx) if len(user.Identities) <= 1 { return unprocessableEntityError(ErrorCodeSingleIdentityNotDeletable, "User must have at least 1 identity after unlinking") } var identityToBeDeleted *models.Identity for i := range user.Identities { identity := user.Identities[i] if identity.ID == identityID { identityToBeDeleted = &identity break } } if identityToBeDeleted == nil { return unprocessableEntityError(ErrorCodeIdentityNotFound, "Identity doesn't exist") } err = a.db.Transaction(func(tx *storage.Connection) error { if terr := models.NewAuditLogEntry(r, tx, user, models.IdentityUnlinkAction, "", map[string]interface{}{ "identity_id": identityToBeDeleted.ID, "provider": identityToBeDeleted.Provider, "provider_id": identityToBeDeleted.ProviderID, }); terr != nil { return internalServerError("Error recording audit log entry").WithInternalError(terr) } if terr := tx.Destroy(identityToBeDeleted); terr != nil { return internalServerError("Database error deleting identity").WithInternalError(terr) } switch identityToBeDeleted.Provider { case "phone": user.PhoneConfirmedAt = nil if terr := user.SetPhone(tx, ""); terr != nil { return internalServerError("Database error updating user phone").WithInternalError(terr) } if terr := tx.UpdateOnly(user, "phone_confirmed_at"); terr != nil { return internalServerError("Database error updating user phone").WithInternalError(terr) } default: if terr := user.UpdateUserEmailFromIdentities(tx); terr != nil { if models.IsUniqueConstraintViolatedError(terr) { return unprocessableEntityError(ErrorCodeEmailConflictIdentityNotDeletable, "Unable to unlink identity due to email conflict").WithInternalError(terr) } return internalServerError("Database error updating user email").WithInternalError(terr) } } if terr := user.UpdateAppMetaDataProviders(tx); terr != nil { return internalServerError("Database error updating user providers").WithInternalError(terr) } return nil }) if err != nil { return err } return sendJSON(w, http.StatusOK, map[string]interface{}{}) } func (a *API) LinkIdentity(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) rurl, err := a.GetExternalProviderRedirectURL(w, r, user) if err != nil { return err } skipHTTPRedirect := r.URL.Query().Get("skip_http_redirect") == "true" if skipHTTPRedirect { return sendJSON(w, http.StatusOK, map[string]interface{}{ "url": rurl, }) } http.Redirect(w, r, rurl, http.StatusFound) return nil } func (a *API) linkIdentityToUser(r *http.Request, ctx context.Context, tx *storage.Connection, userData *provider.UserProvidedData, providerType string) (*models.User, error) { targetUser := getTargetUser(ctx) identity, terr := models.FindIdentityByIdAndProvider(tx, userData.Metadata.Subject, providerType) if terr != nil { if !models.IsNotFoundError(terr) { return nil, internalServerError("Database error finding identity for linking").WithInternalError(terr) } } if identity != nil { if identity.UserID == targetUser.ID { return nil, unprocessableEntityError(ErrorCodeIdentityAlreadyExists, "Identity is already linked") } return nil, unprocessableEntityError(ErrorCodeIdentityAlreadyExists, "Identity is already linked to another user") } if _, terr := a.createNewIdentity(tx, targetUser, providerType, structs.Map(userData.Metadata)); terr != nil { return nil, terr } if targetUser.GetEmail() == "" { if terr := targetUser.UpdateUserEmailFromIdentities(tx); terr != nil { if models.IsUniqueConstraintViolatedError(terr) { return nil, badRequestError(ErrorCodeEmailExists, DuplicateEmailMsg) } return nil, terr } if !userData.Metadata.EmailVerified { if terr := a.sendConfirmation(r, tx, targetUser, models.ImplicitFlow); terr != nil { return nil, terr } return nil, storage.NewCommitWithError(unprocessableEntityError(ErrorCodeEmailNotConfirmed, "Unverified email with %v. A confirmation email has been sent to your %v email", providerType, providerType)) } if terr := targetUser.Confirm(tx); terr != nil { return nil, terr } if targetUser.IsAnonymous { targetUser.IsAnonymous = false if terr := tx.UpdateOnly(targetUser, "is_anonymous"); terr != nil { return nil, terr } } } if terr := targetUser.UpdateAppMetaDataProviders(tx); terr != nil { return nil, terr } return targetUser, nil }