Authentication And Identity Systems

Social Login And Account Linking

Social login ("Sign in with Google/GitHub/Apple") builds on the OAuth/OIDC mechanics, but its hard problems are not cryptographic — they are about identity reconciliation. One human may arrive as a password account today, a Google identity tomorrow, and a GitHub identity next month, and your system has to decide when those are the same person. Get it wrong in one direction and users end up with duplicate accounts; get it wrong in the other and you hand attackers a path to take over accounts they do not own.

The Account Linking Trap

The tempting shortcut is to link by email: a Google login arrives with alice@example.com, you find a local account with that email, and you log them in. This is a well-known account-takeover vulnerability. The problem is that not every provider verifies email ownership, and even those that do are a claim you are choosing to trust. If a provider lets someone sign up with an unverified alice@example.com, an attacker does exactly that, clicks "Sign in with that provider" on your site, and your email-matching logic hands them Alice's existing account.

The safe rules:

  • Key every identity off the provider's stable subject identifier (sub), never off email. Email is a mutable attribute, not an identity.
  • Never automatically link a new provider identity to an existing account by matching email. Automatic linking is the vulnerability.
  • Link only through an authenticated action: the user is already logged in via a proven method, and then connects the new provider from account settings. Proving control of the existing account is what makes linking safe.
  • If a social login arrives whose email matches an existing account but is not yet linked, do not merge — send the user to log in with their existing method first, then offer to link.
PHP example
<?php

declare(strict_types=1);

// An identity is (provider, subject) -> local user. Email is never the key.
$identities = [
    'google:108734...' => 'user_42',
    'github:99120'     => 'user_42',   // same human, linked deliberately
];

function resolveUser(array $identities, string $provider, string $subject): ?string
{
    return $identities[$provider . ':' . $subject] ?? null; // null => new identity, decide carefully
}

echo resolveUser($identities, 'google', '108734...') ?? 'NEW';
echo PHP_EOL;

// Prints:
// user_42

Provider Differences That Bite

Real providers differ in ways that leak into your data model. Some return an email_verified flag — honor it, and treat unverified emails as untrusted. Apple's "Hide My Email" hands you a per-app relay address, not the user's real one, and Apple returns the user's name only on the very first authorization, so if you do not capture it then, it is gone. GitHub users may have no public email at all. The lesson: store the provider identity as the anchor and treat every human-meaningful attribute (email, name, avatar) as optional, provider-specific, and refreshable — never as a primary key.

Designing For Multiple Identities

Model identity as a separate table: one local user has many linked identities, each a (provider, subject) pair, plus one optional password credential. This makes the common flows fall out naturally — a user can add or remove providers, log in by any linked method, and you can present "you already have an account, want to link?" instead of silently forking a duplicate. Two safety rails on top: never let a user unlink their last remaining login method (that locks them out), and require re-authentication before linking or unlinking, because those operations change how the account can be accessed.

What To Check

Before moving on, make sure you can:

  • explain why linking accounts by matching email is an account-takeover vulnerability
  • key identities off (provider, sub) and treat email as a mutable attribute
  • describe safe linking: only through an already-authenticated action
  • name provider quirks (email_verified, Apple relay and first-auth-only name, missing emails)
  • model one user with many identities and prevent last-method removal

What You Should Be Able To Do

After this lesson, you should be able to build social login that reconciles identities safely, link multiple providers to one account without creating a takeover path, and design a data model that survives the messy realities of how different providers report identity.

Practice

Practice: Design Multi-Provider Identity

Design the identity model and linking flows for an app supporting password login plus Google and GitHub sign-in, where one person may use any or all three. Specify the data model, how a new identity is resolved, and how linking happens safely.

Show solution

Resolving an incoming social login: validate the token per the OIDC lesson, then look up (provider, sub). If found, log in that user. If not found, this is a new identity, and the decision is explicit: if nobody is logged in, offer to create a new account or to "log in with your existing method to link this" — never auto-link by email. If someone is already logged in, this is an intentional link from settings: attach the new (provider, sub) to their user after re-authentication.

Safe linking flow: linking is only ever initiated from account settings by an already-authenticated user, and requires a fresh authentication (re-enter password or re-verify a provider) immediately before the change. Unlinking is the same, with the extra rule that the last remaining login method cannot be removed.

Attribute handling: email, name, and avatar from each provider are stored per-identity as refreshable profile data, honoring email_verified where given and treating Apple's relay address and first-auth-only name as the special cases they are. The (provider, sub) pair is the only thing used as an identity key.

The design's core property: two different humans who happen to share an email never collide, and one human's several providers link only through proof of controlling the existing account.

Practice: Diagnose An Account Takeover

An app lets users sign in with Google or a password. When a Google login arrives, the app finds any local account with the same email and logs the user in. An attacker creates a Google account with a victim's email address (which Google let them do without verification at the time) and gains access to the victim's password-based account.

Task

Explain the vulnerability precisely and give the fix.

Show solution

Why each safe rule would have stopped it:

  • Keying off (provider, sub) instead of email: the attacker's Google sub is brand new and matches no local identity, so no existing account is reachable.
  • Never auto-linking by email: even with a matching email, the correct response is "an account exists — log in with your password first, then link Google," which requires the attacker to already control the victim's account.
  • Honoring email_verified: an unverified provider email is untrusted and cannot even be offered for linking.

The fix is structural: incoming social identities create or match only on (provider, sub); any email collision routes to a deliberate, authenticated link, never an automatic one. Since this bug may have been exploited, the response includes auditing recent Google-linked logins and notifying affected users per the application-compromise lesson.

Practice: Define Linking Review Checks
Show solution
  • A test asserts that a social identity with an email matching an existing account, but a new (provider, sub), does not log into or merge with that account automatically — the anti-takeover invariant, tested directly.
  • A test confirms linking requires the user to be authenticated and to re-authenticate immediately before the link, and that unlinking the last login method is refused.
  • A test confirms account lookup never falls back to email matching anywhere in the login path.
  • A test covers provider quirks the app claims to support: unverified email_verified treated as untrusted, and Apple relay/first-auth-name handled without crashing.

Human, periodically:

  • Review any code path that reads email to make sure none of them are making an identity decision — email is display/contact data only.
  • Audit the identities table for anomalies: multiple users sharing a (provider, sub) (should be impossible), or a spike in new links, which can indicate takeover attempts.
  • Re-verify provider email_verified semantics haven't changed, since providers occasionally alter what they guarantee.