Federated login and passkeys
Link federated identities safely
Attach provider identities to local accounts without taking over an existing account through an unverified or colliding email address.
After a federated login, the provider tells you “this is subject 10769150350006150715113082367”. Your job is to connect that external identity to a local Books account. Get this wrong and someone signs in with Google and lands inside another person’s account.
The key is issuer plus subject
The stable external identity is the provider’s sub claim, the subject identifier. It’s the value that doesn’t change when the user renames themselves or changes email. Store it together with the issuer:
issuer + subject
The pair is the uniqueness key. The provider’s display name is not enough, and neither is the subject alone. Two issuers could hand out the same subject string, and one company can operate several issuers. https://accounts.google.com plus 10769... is unambiguous. “Google” plus 10769... is not.
Email is not proof
Here’s the takeover. Alice has a Books account under [email protected], created with a password. An attacker signs up at an identity provider that doesn’t verify emails and claims [email protected] there. They click “Sign in with that provider” on your app. Your app sees a matching email and helpfully logs them into Alice’s account.
So never join accounts automatically because the email text matches. Providers verify addresses with different rigor, organizations recycle addresses when people leave, and claims change over time.
If the user is already signed in and wants to link a new provider, ask for recent authentication, then link. If they’re not signed in and the email collides with an existing account, don’t merge. Run a deliberate flow that proves control of both sides, for instance by logging into the existing account with its password first.
Enforce it in the database
One external identity must map to exactly one local user. Put a unique constraint on (issuer, subject) in the external_identities table. The application check can race, the constraint can’t.
Record who initiated the link, when, and which session approved it. Linking is an audit event.
Unlinking is sensitive too
Unlinking can remove the user’s only way in. Before allowing it, check that another usable credential remains: a password, a passkey, or another provider. And check that a recovery path still exists.
Treat both link and unlink as step-up actions. Require recent authentication, and notify the verified email so the real owner learns about it if it wasn’t them.
Try this: design the users and external_identities tables for the Books API and the flow for adding a second provider. Then write two tests. In the first, a provider identity arrives with an email matching an existing password account and must not be logged in automatically. In the second, an already-linked (issuer, subject) pair tries to attach to a second local user and must fail on the constraint.
Lesson completed