Prompt
Can you help me design a login flow that supports Google, Apple, and Microsoft sign in plus account linking?
Latest observation
Absolutely — here’s a solid, modern login/account-linking design that supports Google, Apple, and Microsoft while avoiding duplicate accounts and making linking safe.
1) Core principle
Use a single internal user account and attach one or more external identities to it.
Data model
users
idemail(optional but useful)email_verifiednamecreated_atupdated_at- other profile fields
auth_identities
iduser_id→ FK tousers.idprovider→google | apple | microsoftprovider_subject→ stable unique ID from provider (sub,oid, etc.)provider_email→ email returned by providerprovider_email_verifiedcreated_atlast_login_at
Uniqueness constraints
- Unique on
(provider, provider_subject) - Optional unique on
emailinusersif your product requires one account per email
This lets one user link:
- Apple
- Microsoft
- potentially email/password later too
2) Recommended sign-in behavior
On successful OAuth/OIDC callback:
- Validate provider tokens.
- Extract:
- stable provider user ID
- email verification status
- Determine whether this login should:
- create a new user
- log into an existing linked account
- prompt account linking
3) Account resolution logic
Use this order:
A. If provider identity already linked
If (provider, provider_subject) exists:
- log the user into that
user_id
B. Else if email matches an existing user
If provider returns a verified email that matches an existing user:
- do not auto-merge silently
- show a “We found an existing account” linking flow
Why? Because:
- Apple may hide relay emails
- Microsoft/Google emails can differ across work/personal accounts
- email alone is not always a secure proof of identity
C. Else create a new user
If no existing identity and no safe email match:
- create new
usersrow - create linked
auth_identitiesrow - log in
4) Safe account linking flow
Linking should happen only when the user is already authenticated in your app.
Linking steps
- User signs into app with existing account.
- User goes to “Connected accounts”.
- Clicks “Link Google / Apple / Microsoft”.
- OAuth flow starts.
- On callback:
- validate provider token
- check if that provider identity is already linked to another user
- if yes, block and explain
- otherwise attach identity to current user
Important rule
If the provider identity is already linked to a different user:
- do not merge automatically
- require support/manual review or special recovery flow
5) Best practice for “first login” vs “linking”
You need to distinguish the intent:
login flow
Used when user wants to sign in.
link flow
Used when logged-in user wants to add another provider.
Implementation-wise:
- add a state/nonce value that includes intent
- store the current app session for linking
- on callback, process accordingly
6) Suggested sign-in UX
On login page
Buttons:
- Continue with Google
- Continue with Apple
- Continue with Microsoft
After OAuth callback:
If matched by linked identity
- sign in immediately
If matched by verified email but not linked
Show:
- “An account already exists with this email”
- options:
- Sign in another way
- Send magic link / email verification
- If already authenticated, link this provider
- Contact support if you can’t access the original account
If new user
- create account automatically
- optionally ask for profile completion later
7) Handling Apple specifically
Apple is a little special:
- The real stable identifier is the Apple
sub - Email may be:
- real email
- private relay email
- missing on later sign-ins
- Name may only be provided on the first consent
Apple rule of thumb
Never rely on Apple email as the unique identifier.
Use Apple sub for identity linking.
8) Handling Microsoft specifically
Microsoft accounts can be:
- personal Microsoft accounts
- work/school Entra ID accounts
Recommendation
Decide whether you support:
- personal accounts only
- both personal + work/school
Store provider metadata so you can distinguish tenant/account type if needed.
9) Suggested callback decision table
| Existing linked identity? | Verified email matches user? | Action |
|---|---|---|
| Yes | any | Sign in existing user |
| No | Yes | Prompt to link / confirm account ownership |
| No | No | Create new user |
| Yes but linked to another user | any | Reject + manual resolution |
10) Security considerations
Use OIDC, not just OAuth
Prefer OpenID Connect for authentication because you get:
- ID token
- verified subject
- email claims where available
Validate:
- issuer
- audience/client_id
- signature
- expiration
- nonce
- state
Never:
- trust provider email without checking verification
- auto-merge accounts solely by matching email
- link identities without the user being authenticated or explicitly confirming ownership
11) Recovery flows you should plan for
Case 1: User signed up with Google, now tries Apple with same email
- If not already linked, ask them to sign in to the original account first and then link Apple.
Case 2: User loses access to original provider
- offer support-assisted recovery
- or allow verified email recovery if your risk tolerance permits
Case 3: Apple relay email changes
- identity still works because you key off Apple
sub
Case 4: Microsoft email differs from Google email for same person
- these are separate accounts unless user links them
12) Practical implementation pattern
Pseudocode
onOAuthCallback(provider, idToken, accessToken, intent, currentUser):
claims = verifyAndDecode(idToken)
providerSubject = claims.sub
email = claims.email
emailVerified = claims.email_verified
existingIdentity = findIdentity(provider, providerSubject)
if existingIdentity:
logIn(existingIdentity.user_id)
return
if intent == "link":
if currentUser is null:
reject("Must be signed in to link accounts")
if identityExistsForAnotherUser(provider, providerSubject):
reject("This provider account is already linked elsewhere")
createIdentity(user_id=currentUser.id, provider, providerSubject, email, emailVerified)
return success("Linked")
existingUser = findUserByVerifiedEmail(email)
if existingUser:
showLinkOrRecoverFlow(existingUser)
return
user = createUser(email=email, email_verified=emailVerified)
createIdentity(user.id, provider, providerSubject, email, emailVerified)
logIn(user.id)
13) Recommendation for your exact product
If you want the safest and simplest approach:
- Use OIDC for Google, Apple, Microsoft
- Store one internal user table
- Store provider identities separately
- Login by linked identity
- Only link while the user is already authenticated
- Do not auto-merge on email alone
- Allow email match to trigger a recovery/linking prompt
14) Good default policy
Here’s a sensible policy to start with:
- Login by provider subject
- Link only from an authenticated session
- If email matches but identity not linked, prompt the user
- If an identity is already linked to another user, block
- Apple
subis source of truth - Require verified emails where available
- No silent merges
If you want, I can also turn this into:
- a sequence diagram,
- a database schema, or
- backend endpoint design for
/auth/callback,/auth/link, and/auth/unlink.