laravel 7 min read

Laravel Socialite: Google, GitHub, and LinkedIn OAuth Login Complete Guide

Add social login to your Laravel application in under an hour. This guide covers Google, GitHub, and LinkedIn OAuth with Laravel Socialite — including account linking, team auto-provisioning, and production gotchas.

G
Gurpreet Singh
March 13, 2026

Why Social Login Matters for SaaS

Password-based signup has a dirty secret: around 60–70% of users who abandon a registration form do so because they do not want to create yet another password. Social login with Google or GitHub removes that friction entirely. Users click one button, grant permissions, and land on your dashboard — no email verification loop, no password to forget.

For B2B SaaS targeting developers, GitHub login is a trust signal. For products targeting professionals more broadly, Google login (via Google Workspace accounts) converts better because users are already logged into Google all day. LinkedIn is worth adding if your product targets recruiters, sales professionals, or HR teams.

Laravel Socialite handles all three providers with a consistent API, and the total implementation time from installation to working production login is under an hour.

Installation and Configuration

composer require laravel/socialite

Add your OAuth app credentials to .env. You get these by registering your application in each provider's developer console:

# Google (console.cloud.google.com → APIs & Services → Credentials)
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...
GOOGLE_REDIRECT_URI=https://yourapp.com/auth/google/callback

# GitHub (github.com → Settings → Developer Settings → OAuth Apps)
GITHUB_CLIENT_ID=Iv1.xxx
GITHUB_CLIENT_SECRET=xxx
GITHUB_REDIRECT_URI=https://yourapp.com/auth/github/callback

# LinkedIn (developer.linkedin.com → My Apps)
LINKEDIN_CLIENT_ID=xxx
LINKEDIN_CLIENT_SECRET=xxx
LINKEDIN_REDIRECT_URI=https://yourapp.com/auth/linkedin/callback

Register the credentials in config/services.php:

'google' => [
    'client_id'     => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect'      => env('GOOGLE_REDIRECT_URI'),
],
'github' => [
    'client_id'     => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect'      => env('GITHUB_REDIRECT_URI'),
],
'linkedin-openid' => [
    'client_id'     => env('LINKEDIN_CLIENT_ID'),
    'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
    'redirect'      => env('LINKEDIN_REDIRECT_URI'),
],

Note: for LinkedIn, use the driver name linkedin-openid (not linkedin) — this uses LinkedIn's newer OpenID Connect flow which returns a verified email address reliably.

Database Migration

Add social login columns to your users table. You need to store the provider name and the provider's user ID so you can match returning users:

Schema::table('users', function (Blueprint $table) {
    $table->string('provider')->nullable();
    $table->string('provider_id')->nullable()->unique();
    $table->string('avatar')->nullable();
    $table->string('password')->nullable()->change(); // password is optional for social users
});

Making password nullable is important — a user who registers via Google never sets a password, so the column must allow null.

Routes and Controller

// routes/web.php
Route::get('/auth/{provider}',          [SocialAuthController::class, 'redirect'])->name('social.redirect');
Route::get('/auth/{provider}/callback', [SocialAuthController::class, 'callback'])->name('social.callback');

This single pair of routes handles all providers via the {provider} wildcard. Add a whitelist to prevent abuse:

// app/Http/Controllers/SocialAuthController.php
namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;

class SocialAuthController extends Controller
{
    private const ALLOWED_PROVIDERS = ['google', 'github', 'linkedin-openid'];

    public function redirect(string $provider)
    {
        abort_unless(in_array($provider, self::ALLOWED_PROVIDERS), 404);

        return Socialite::driver($provider)->redirect();
    }

    public function callback(string $provider)
    {
        abort_unless(in_array($provider, self::ALLOWED_PROVIDERS), 404);

        try {
            $socialUser = Socialite::driver($provider)->user();
        } catch (\Exception $e) {
            return redirect('/login')->withErrors(['social' => 'Authentication failed. Please try again.']);
        }

        $user = $this->findOrCreateUser($socialUser, $provider);

        auth()->login($user, remember: true);

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

    private function findOrCreateUser($socialUser, string $provider): User
    {
        // 1. Try to find by provider + provider_id (returning social user)
        $user = User::where('provider', $provider)
                    ->where('provider_id', $socialUser->getId())
                    ->first();

        if ($user) {
            // Update avatar in case it changed
            $user->update(['avatar' => $socialUser->getAvatar()]);
            return $user;
        }

        // 2. Try to find by email (existing password user — link accounts)
        if ($existing = User::where('email', $socialUser->getEmail())->first()) {
            $existing->update([
                'provider'    => $provider,
                'provider_id' => $socialUser->getId(),
                'avatar'      => $socialUser->getAvatar(),
            ]);
            return $existing;
        }

        // 3. Create a new user
        $user = User::create([
            'name'        => $socialUser->getName(),
            'email'       => $socialUser->getEmail(),
            'provider'    => $provider,
            'provider_id' => $socialUser->getId(),
            'avatar'      => $socialUser->getAvatar(),
            'email_verified_at' => now(), // OAuth email is already verified
        ]);

        // Auto-provision a team for new users
        $team = $user->teams()->create(['name' => $user->name . "'s Workspace"]);
        session(['current_team_id' => $team->id]);

        return $user;
    }
}

The three-step lookup is the most important part: check for an existing social connection, then fall back to email matching (so a user who previously signed up with a password can link their Google account), then create a new user. This prevents duplicate accounts — the most common social login bug in production.

Login Buttons in Blade

{{-- resources/views/auth/login.blade.php --}}


or sign in with email
{{-- standard email/password form below --}}

Scopes: Requesting Additional Permissions

By default, Socialite requests the minimum scopes needed to get a user's name and email. If you need additional data — like a user's GitHub repositories or Google Calendar access — request extra scopes on the redirect:

// Request read access to GitHub repos (for a developer tool)
return Socialite::driver('github')
    ->scopes(['read:user', 'repo'])
    ->redirect();

// Request Google Calendar access (for a scheduling tool)
return Socialite::driver('google')
    ->scopes(['openid', 'profile', 'email', 'https://www.googleapis.com/auth/calendar.readonly'])
    ->with(['access_type' => 'offline', 'prompt' => 'consent']) // get refresh token
    ->redirect();

Store the access token and refresh token returned by $socialUser->token and $socialUser->refreshToken if you need to make API calls on behalf of the user after login.

Production Gotchas

Redirect URI Must Match Exactly

Google and LinkedIn reject the OAuth callback if the redirect URI in your request does not exactly match what is registered in the provider console — including trailing slashes, http vs https, and the domain. In production, always use HTTPS. Add both your production URL and local URL to the allowed redirect URIs during development.

Set HTTPS for Local Testing

Use Laravel Valet's valet secure yourapp or Expose (expose share) to get a public HTTPS URL for testing OAuth flows locally. OAuth providers will not send callbacks to plain http://localhost for most providers.

Handle the Cancelled Flow

If the user clicks "Cancel" on the provider's consent screen, they are redirected back with an error parameter. The try/catch in the callback handles this, but make sure your error message is user-friendly — not a raw exception stack trace.

Frequently Asked Questions

Can a user have multiple social providers linked?

Yes, but you need to change the data model. Instead of storing provider and provider_id on the users table, create a separate social_accounts table: id, user_id, provider, provider_id, token, refresh_token. This lets one user link Google, GitHub, and email/password simultaneously. It is more complex to implement but significantly better UX for users who use different devices or contexts.

What if the provider does not return an email?

Some GitHub accounts have private email settings — Socialite returns null for the email. Handle this: if $socialUser->getEmail() is null, redirect the user to a "complete your profile" page where they enter an email manually before their account is created. Do not create an account with a null email — you will not be able to send them transactional mail.

How do I allow existing password users to link social accounts?

The email-matching logic in findOrCreateUser() already handles this — if an existing user with the same email logs in via Google, their account is automatically linked. If you want explicit "Connect your Google account" functionality in a settings page, add a separate route that is protected by auth middleware, skips the findOrCreate logic, and directly updates the current user's provider and provider_id.

#Laravel #OAuth #Socialite #Google Login #GitHub Login #Authentication #SaaS #Startup
G
Gurpreet Singh

Senior Full Stack Developer — Laravel, Vue.js, Nuxt.js & AI. Available for freelance projects.

Hire Me for Your Project

Related Articles