fix(api): create email identity when setting password on OAuth-only user#2509
Open
mastermanas805 wants to merge 1 commit intosupabase:masterfrom
Open
Conversation
UserUpdate handles a password change by writing the new hash to the users table but never creates a corresponding email identity row, even though the user now has email/password as a valid sign-in method. For users whose only existing identity is from an external OAuth provider (custom OIDC, Apple, GitHub, etc.), this leaves auth.identities and the providers list in app_metadata out of sync with what /user reports and what the user can actually do. Mirror the pattern already used by recoverVerify / emailChangeVerify in verify.go: after UpdatePassword succeeds, look up an email identity for the user, and if there isn't one and the user has an email address, create it via createNewIdentity and refresh app_metadata.providers. The existing operation is idempotent for callers that already have an email identity. Adds a regression test that creates a user with a single external identity, asserts no email identity exists initially, calls PUT /user with a password, and asserts an email identity is now present and is not duplicated on a second password change. Fixes supabase#2085 Signed-off-by: Manas Srivastava <mastermanas805@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes #2085.
When a user whose only existing identity is from an external OAuth provider (custom OIDC, Apple, GitHub, etc.) calls
PUT /userwith apasswordfield,UserUpdatewrites the new password hash to the users table but does not create a correspondingemailidentity row inauth.identities. After the call, the user's password works, butapp_metadata.providersstill lists only the external provider,/user/identitiesdoesn't show an email identity, and downstream code that gates on identity rows treats the user as not having email/password.The recovery and email-change flows in
verify.goalready do this correctly viacreateNewIdentity(tx, user, "email", ...)plusUpdateAppMetaDataProviders.UserUpdatewas the missing copy.How to reproduce on master (for reviewers)
This needs a user whose only identity is non-email. Easiest way is the custom OIDC flow against a local IdP (Zitadel works), but any OAuth provider will do. Steps:
Now create an OAuth-only user. The fastest path is the unit-style reproducer below, but anyone with a working OIDC provider can drive this through
/authorize?provider=...and complete the OAuth dance.DB inspection on master:
Live reproduction I ran
Drove the full custom-OIDC flow against a local Zitadel instance configured as a custom OIDC provider in auth, captured the access token, ran the above PUT, and confirmed:
identities = ['custom:zitadel']identities = ['custom:zitadel']← BUGidentities = ['custom:zitadel', 'email']← FIXEDThe DB row added by the fix has
provider='email',provider_id=<user.ID>,identity_datacontainingsub,email, andemail_verifiedmirroring the user record.Root cause
internal/api/user.go::UserUpdate. Afteruser.UpdatePassword(tx, sessionID)succeeds, the function continues with audit log + notification but never creates an email identity. Compare with the recovery and email-change flows ininternal/api/verify.gowhich callcreateNewIdentity(tx, user, "email", structs.Map(provider.Claims{...}))on the same condition.Fix
In the password-update branch of the transaction in
UserUpdate, after the audit log and password-changed notification, look upmodels.FindIdentityByIdAndProvider(tx, user.ID.String(), "email"). If the user has an email and no email identity exists, create one viaa.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{...}))and calluser.UpdateAppMetaDataProviders(tx)soapp_metadata.providersreflects the new provider. Idempotent for callers that already have an email identity.How to verify the fix manually (for reviewers)
Apply this PR's diff, hot-reload auth (
docker compose -f docker-compose-dev.yml restart author just save the file under CompileDaemon), and re-run the PUT step from the reproduction. The/userresponse now lists the email identity. The DB:Automated tests
internal/api/user_test.goaddsTestUserUpdatePasswordCreatesEmailIdentity:PUT /userwith{"password": "..."}PUT /userwith{"password": "...", "current_password": "..."}and asserts the email identity is not duplicatedRun:
go test ./internal/api/ -run TestUser/TestUserUpdatePasswordCreatesEmailIdentity -v -count=1Fails on master (
require.NoErroron the post-PUT identity lookup), passes after this fix.Verification I ran locally
go test ./internal/api/ -run TestUser -v -count=1— green (existing TestUserUpdatePassword* cases unchanged + new case passes)go test ./... -count=1 -p 1 -race— full module suite greengofmt -l internal/api/user.go internal/api/user_test.go— emptygo vet ./...— emptyNotes for review
user.GetEmail() != "".EmailVerifiedon the new identity reflectsuser.IsConfirmed()so an unverified email stays unverified.internal/api/verify.go::recoverVerifyandemailChangeVerifyso a reader will recognize the shape.