skip to content

On a Laravel site with GitHub and Google sign-in, why is linking the Socialite user to a local account by email alone dangerous, and what should the callback do instead?

level: seniorimportance: should knowfreq 42%

answer

  1. provider plus provider id
  2. email may be null or unverified
  3. a pre-registered unverified local account
  4. Google's raw email_verified claim
  5. link while signed in, not silently

basics

~20 s

Email-only matching lets anyone who controls an address at a provider, or pre-registered it locally unverified, take over the account. Key links on provider plus getId(); merge by email only when both sides are verified.

solid answer

~50 s

The callback should look up a link row by **provider and `getId()`**, never by email alone. Emails are weaker keys: GitHub may return null, a provider may hand back an address it has not verified, and an attacker can pre-register a local account with a victim's email and never verify it, waiting for the victim's first social login to be merged into it. Safer rules for a Laravel callback: store `(provider, provider_id)` in a table with a unique index; when no link exists, auto-link by email only if the provider says the email is verified (Google's raw `email_verified`, or the GitHub driver's primary verified address fetched under `user:email`) **and** the local user has `email_verified_at` set; otherwise create a new account or ask the member to sign in and link from their settings. Keep provider tokens out of plain columns.

go deeper

for a junior

Know that the callback must map the provider user to a local account, and that the provider id, not the email, is the safe key.

for a middle

Explain why emails can be null or unverified and how a separate provider-link table with a unique index models several providers per member.

for a senior

Walk through the pre-registration hijack and the rule that both the provider's and the local account's email must be verified before auto-linking.

for a principal

Decide the product rule for merging identities across providers and how support recovers members locked out by a wrong link.

## The naive callback A developer-community site adds GitHub and Google sign-in, and the callback does the obvious thing: ```php $social = Socialite::driver($provider)->user(); $user = User::firstOrCreate(['email' => $social->getEmail()], [...]); Auth::login($user); ``` It works in the demo and fails in three ways in production. ## How email-only matching goes wrong 1. **Null emails.** The GitHub driver returns the primary verified address only when `user:email` is requested, and `getEmail()` can still be null. If the column allows nulls, `firstOrCreate(['email' => null])` becomes a `whereNull` lookup and can match another member who also has no email. 2. **Unverified provider emails.** Some providers let a member set an address they have not proven they own. Trusting it lets an attacker type the victim's email at the provider and be logged into the victim's local account. 3. **Pre-registration hijack.** The attacker signs up on your site with the victim's email and a password, never verifies it, and waits. When the victim later clicks "Sign in with Google", email matching merges the victim into the attacker's account, and the attacker's password still works. ## A safer data model Store links separately from users: | Column | Purpose | |---|---| | `user_id` | the local account | | `provider` | `github`, `google` | | `provider_id` | `getId()` from Socialite | | unique index on (`provider`, `provider_id`) | one local owner per provider identity | `getId()` is stable: members can rename themselves on GitHub or change their Google address, and the id stays the same. ## A safer callback 1. Resolve the provider user with `Socialite::driver($provider)->user()`. 2. Look up the link by `provider` and `provider_id`. If found, sign in that user. 3. If not found and an email is present, decide whether the provider **asserts** it verified: - for Google, the raw profile includes `email_verified` (`$social->user['email_verified']`); - for GitHub with `user:email`, the driver only returns an address marked primary and verified. 4. Auto-link to an existing local account only if the provider asserts verification **and** that local user has `email_verified_at` set. An unverified local account is exactly the pre-registration trap. 5. Otherwise, create a new account, or tell the member "an account with this email exists; sign in and connect GitHub from your settings". 6. Sign in through the session guard, and regenerate the session as with any login. ```php $link = SocialAccount::where('provider', 'google') ->where('provider_id', $social->getId()) ->first(); if (! $link && $social->getEmail()) { $existing = User::where('email', $social->getEmail())->first(); $trusted = ($social->user['email_verified'] ?? false) === true && $existing?->hasVerifiedEmail(); // link only when $trusted, else create or ask to link manually } ``` ## Unlinking and the last sign-in method Once members can attach several providers, they will also want to detach them. Refuse to remove the last way in: if a member has no password and only one linked provider, unlinking it leaves an account nobody can reach. Show which providers are linked, and when each was last used, on the settings page, so a member notices a link they did not create. ## Linking from the settings page When the member is already signed in, linking is safe: the current session proves who owns the local account, and the callback just adds a row for `Auth::user()`. That is the path for members whose emails differ between providers. ## Storing provider tokens The docs' example writes `github_token` and `github_refresh_token` onto the user. If you keep them, treat them as credentials: use an `encrypted` cast, keep them out of serialized output, and store only when a feature actually calls the provider's API. ## Summary - Key on provider plus provider id. - Merge by email only when both sides are verified. - Otherwise link explicitly from a signed-in session.

  • How does a pre-registration attack work against a Laravel app that links social logins by email?
    The attacker registers locally with the victim's email and a password they know, and never verifies it. When the victim later signs in with Google, a callback that matches on email logs the victim into that same account. The attacker can still sign in with the password and sees everything the victim does. Requiring `email_verified_at` on the local account before auto-linking closes it.
  • Where does Socialite expose whether Google verified the email address?
    In the raw profile: `$social->user['email_verified']`, also available through `getRaw()`. The Google driver maps `sub` to `getId()` and keeps the other claims in the raw array; it does not refuse unverified emails for you, so the callback has to read the flag itself.

saying these in an interview costs you the question

  • An email from any OAuth provider has always been verified by that provider
  • firstOrCreate on email is safe because emails are unique in the users table
  • The GitHub nickname is a stable key for linking accounts
  • Auto-linking to an unverified local account is fine if the provider verified the email
  • Socialite refuses to return users whose email is unverified