Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

How "Sign in With Google" Works Behind the Button (and the Checks You Shouldn't Skip)

About Post

"Sign in with Google" is the friendliest button on the internet. One click, pick an account, and you're in. No password to forget, no confirmation email to dig out of spam.

Behind that click, though, there are six distinct steps, a client secret, a signed token and several checks that, if you skip them, turn the friendliest button into a very polite way to let the wrong person into an account.

I covered OAuth in general in an earlier post. This one zooms into the exact thing most of us actually build: Google login in a Laravel app. What happens, what the token really contains, and the checks you shouldn't skip.

OAuth vs OpenID Connect, in two sentences

OAuth 2.0 is about access: "let this app read my calendar". It hands out access tokens and says nothing about who the user is.

OpenID Connect (OIDC) is a thin layer on top of OAuth that adds identity: "this is user X, and Google vouches for it". "Sign in with Google" is OIDC. The key addition is the ID token.

What happens after the click

  1. Your app redirects the browser to Google with your client ID, the scopes you want (openid email profile), the URL to come back to, and a random state value it has also saved in the user's session.
  2. Google handles the login. Password, two-factor, account picker, consent screen. Your app never sees any of it, which is the whole point.
  3. Google redirects back to your callback URL with a short-lived authorisation code and the same state.
  4. Your app checks the state against the session. If it doesn't match, someone may be trying to slip their own login into your user's browser, so you stop.
  5. Your server exchanges the code with Google, server to server, using your client secret. Back comes an access token and an ID token.
  6. Your app reads who the user is, finds or creates their account, and starts a normal session.

Notice what never touches the browser: the client secret and the token exchange. That's why the authorisation code flow is the right one for server-rendered apps. (For apps that can't keep a secret, like SPAs and mobile apps, the same flow is used with PKCE, which proves the app that started the login is the same one finishing it.)

What's inside the ID token

An ID token is a JWT: a signed JSON payload. Decoded, the interesting part looks something like this:

{
  "iss": "https://accounts.google.com",
  "aud": "1234567890-abc.apps.googleusercontent.com",
  "sub": "110169484474386276334",
  "email": "[email protected]",
  "email_verified": true,
  "name": "Sara Ahmed",
  "iat": 1786000000,
  "exp": 1786003600
}
  • iss: who issued it. Must be Google.
  • aud: who it was issued for. Must be your client ID, not some other app's.
  • sub: the user's permanent, unique Google ID. This is the one to store.
  • email_verified: whether Google has verified the email address.
  • exp: when it expires.

The most important line there is sub. Emails can change. sub doesn't.

The Laravel side with Socialite

Laravel Socialite handles the redirect, the state check and the code exchange for you. After adding your Google credentials to config/services.php, the two routes look like this (simplified):

Route::get('/auth/google', fn () => Socialite::driver('google')->redirect());

Route::get('/auth/google/callback', function () {
    $google = Socialite::driver('google')->user();

    abort_unless($google->user['email_verified'] ?? false, 403);

    $user = User::firstWhere('google_id', $google->getId());

    if (! $user) {
        abort_if(User::where('email', $google->getEmail())->exists(), 409,
            'Log in with your password first, then connect Google.');

        $user = User::create([
            'google_id' => $google->getId(),
            'name' => $google->getName(),
            'email' => $google->getEmail(),
        ]);
    }

    Auth::login($user, remember: true);

    return redirect()->intended('/dashboard');
});

In a real app you'd show a friendly page instead of the aborts, and you'd catch Socialite's InvalidStateException (an expired session or a replayed callback) and send the user back to the login page.

The checks that actually matter

Match on sub, not on email

❌ "Find the user with this email and log them in."

✅ Store Google's ID (sub, which Socialite returns from getId()) and look users up by it. Email is for contact, not for identity.

Be careful when linking to an existing account

The tricky case: someone signs in with Google, and a password account with that email already exists. Auto-linking feels helpful, but it means anyone who controls that email at a provider can walk into the existing account. The safer pattern, used above, is to ask the user to log in the old way first and then connect Google from their settings. If you do auto-link, at least require email_verified.

Don't turn off state

Socialite has a stateless() method for API-style flows. In a normal web app, leave it alone: the state check is your protection against login CSRF.

Restricting to a company domain? Verify it yourself

If only your organisation's Google Workspace accounts should get in, passing a domain hint to Google just changes the account picker. It isn't enforcement. Check the hd claim (the Workspace domain) on the server, and reject anything else.

Mobile apps: verify the token on the server

A React Native app using Google's native sign-in gets an ID token and sends it to your API. Never trust it just because it decodes. Verify the signature against Google's public keys and check iss, aud and exp, ideally with Google's official client library rather than hand-rolled JWT code. A token issued for some other app has a different aud, and that check is what stops someone replaying it against yours.

The rule: Google proves who the user is. Your app still decides which account that person gets and what they're allowed to do. Identity comes from sub; authorisation stays yours.

Checklist

  • Authorisation code flow, client secret only on the server (PKCE for public clients).
  • state checked on every callback.
  • Users matched by sub, not email.
  • No silent linking to existing password accounts.
  • Domain restrictions enforced server-side.
  • ID tokens from mobile apps verified: signature, iss, aud, exp.

The official Google OpenID Connect guide and the Laravel Socialite docs are both worth a read before you ship.

Do you link social logins to existing accounts automatically, or make users connect them by hand? I'd like to hear how others handle that edge case.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close