Social Login in Laravel 13 with Socialite: Google and GitHub Step by Step
Social login in Laravel 13 with Socialite boils down to two methods, redirect() and user(), but the real work lives in the setup: credentials, redirect URIs, scopes, and creating the local user. This guide walks through Google and GitHub end to end.
What Socialite Is and Why You Should Use It
Laravel Socialite is the official package of the Laravel ecosystem that wraps the OAuth 1 and OAuth 2 libraries. Instead of hand-rolling the code-for-token exchange, the anti-CSRF state handling, and the parsing of provider responses, it leaves you with two calls: redirect the user to the provider and fetch their profile when they come back. By mid-2026 it has passed 84 million downloads on Packagist, its current branch is v5.x, and it is compatible with Laravel 13, released on March 17, 2026 with PHP 8.3 as the minimum version.
OAuth 1 and OAuth 2 Without the Boilerplate: Two Methods Instead of Ten Steps
Without Socialite, "Sign in with Google" means building the authorization URL by hand, validating the state parameter, exchanging the code for an access token, and requesting the profile. With Socialite, the redirect controller returns a response that sends the user to Google, and the callback receives an object with the profile data. The entire HTTP dance stays inside the package, and your code only decides what to do with that user.
Official and Community Providers (SocialiteProviders)
The Laravel 13 documentation lists Facebook, X, LinkedIn, Google, GitHub, GitLab, Bitbucket, and Slack out of the box, and the README adds Twitch. For the rest, the SocialiteProviders ecosystem maintains dozens of adapters with the same API: you install the package, register its listener, and use the driver with the usual pattern. One learning curve, regardless of the provider.
Read also
Installation and Configuration
Installing Socialite is one Composer command and, after that, two configuration blocks: one in config/services.php and another in the provider's dashboard.
composer require laravel/socialite and the config/services.php Entry
composer require laravel/socialite:^5.0Credentials live in config/services.php under each provider's key, always through environment variables and never hard-coded:
'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'),
],The redirect field is the flow's callback URL and must match the authorized URI at the provider exactly. It is the first place to look when something goes wrong.
Credentials in Google Cloud Console: OAuth Client ID and Redirect URIs
In Google Cloud Console you create a project, configure the OAuth consent screen, and create a credential of type "OAuth client ID" for a web application. Google hands you the client ID and client secret, and asks you to register the authorized redirect URIs: that is where you put your callback, for example https://your-blog.com/auth/callback/google.
Credentials in GitHub OAuth Apps
On GitHub the equivalent is an OAuth App: Settings, Developer settings, OAuth Apps, and create a new one. You register the Homepage URL and the Authorization callback URL, and GitHub gives you the client ID and secret right away. Unlike Google, there is no consent screen to configure in advance: the user authorizes the app at login time.
The Most Common Error: redirect_uri Doesn't Match the Authorized URI
The classic failure in any OAuth integration is "Invalid redirect_uri" or "Missing required parameter client_id". The cause is almost always the same: the redirect value in config/services.php does not match the authorized URI character by character, or the client_id environment variables are not set and Laravel sends an empty string. Check your .env, then the registered URI, and run php artisan config:clear after changing any value.
The Two Routes of the OAuth Flow
The flow is built with two routes: one that redirects to the provider and one that receives the user on the way back. In web.php, using GitHub as the example:
Route::get('/auth/redirect', function () {
return Socialite::driver('github')->redirect();
});
Route::get('/auth/callback', function () {
$user = Socialite::driver('github')->user();
// Create or update the local user and log in
});Redirecting to the Provider with ->redirect()
redirect() builds the authorization URL, adds the anti-CSRF state parameter, and returns a redirect response. In the view, the "Sign in with GitHub" button is a link or form pointing to /auth/redirect; the user authorizes on GitHub and comes back with a code your callback will exchange for the profile.
Receiving the User in the Callback with ->user()
In the callback, user() exchanges the code for an access token and requests the profile from the provider. The resulting object exposes token, refreshToken (not always available), and expiresIn, plus the profile data. If the exchange fails, catch the exception and show a friendly message instead of a 500 error.
Creating or Updating the Local User
With the provider profile in hand, you decide what happens in your database. The recommended pattern for a blog like blenderdeluxe: create the user on first visit or update their data if they already exist, then log them in.
The $user Data: ID, Name, Email, and Avatar
The object returned by user() is read through methods: getId(), getName(), getEmail(), getAvatar(), and getNickname(). With those you fill your users table; the avatar deserves a note in the security section, because storing the remote URL as-is is not a good idea.
Linking by provider_id and provider, Not by Email Alone
The key to a correct social login is linking the local account by the provider and provider_id pair, not by email alone: the email may not be verified by the provider, or it may come back as null (the GitHub case). The updateOrCreate pattern:
$user = User::updateOrCreate(
['provider' => 'github', 'provider_id' => $githubUser->getId()],
[
'name' => $githubUser->getName() ?? $githubUser->getNickname(),
'email' => $githubUser->getEmail(),
'avatar' => $githubUser->getAvatar(),
]
);
Auth::login($user);
return redirect('/dashboard');Linking by provider_id means that if the provider email changes, your local user stays the same; and it prevents an attacker with an unverified email from attaching their account to someone else's.
Scopes, Domains, and Stateless Mode
By default Socialite requests the provider's basic scopes. When you need more data, or you want to restrict who can sign in, three tools come into play.
scopes() and setScopes(): Ask for Only What You Need
scopes() adds permissions to the ones the driver requests by default, and setScopes() replaces them entirely. For example, to request a GitHub user's public repos in the same authorization:
return Socialite::driver('github')
->scopes(['read:user', 'public_repo'])
->redirect();The rule is to ask for the minimum: the more scopes you request, the more suspicion the consent screen raises and the more risk surface you accumulate.
Restricting to a Corporate Domain with with(['hd' => ...])
For an internal app in a company on Google Workspace, you limit login to a specific domain with the hd (hosted domain) parameter:
return Socialite::driver('google')
->with(['hd' => 'yourcompany.com'])
->redirect();If a user tries to sign in with an account from another domain, Google rejects it on its own screen, before it ever reaches your callback.
stateless() for APIs and Session-Free SPAs
When the frontend is a SPA or a session-free API, the state parameter has nowhere to be stored. Socialite offers stateless() for that scenario: the flow completes without depending on the session. It is the typical pattern for social login from decoupled frontends and pairs well with the token-based approach for Laravel 13 APIs with Sanctum covered on this blog.
$user = Socialite::driver('google')->stateless()->user();The GitHub Gotcha: Private Emails
GitHub lets users hide their email on their profile. When that happens, getEmail() returns null and your updateOrCreate would store an empty email, breaking any logic that depends on it. The fix: request the user:email scope, which authorizes reading the address even when it is private, or handle the null explicitly, for example by redirecting to a form that asks for the email as a second step. The worst option is assuming the email always arrives.
Security in Social Login
Socialite handles the anti-CSRF state parameter for you, so the weak link is usually not the package but what you do with the data you receive.
Validating Verified Emails and Not Trusting Remote Avatars
If your app treats the email as verified, make sure the provider confirms it: on Google the ID token carries email_verified, and on GitHub the email is verified by default when public. Also, do not use the remote avatar URL in your views: download the image to your own storage at signup time, as covered in this blog's file upload guide, and store the local path in the users table.
Duplicate Accounts and Token Rotation
Two different providers can return the same email for different accounts: that is why the provider/provider_id pair is the only reliable key. Store the access token only if you are going to use it; if not, do not persist it. And remember it expires: your app must refresh it with the refreshToken before using it again.
Testing the Flow with Socialite::fake()
Testing the callback with real HTTP calls is slow and fragile. Socialite ships Socialite::fake(), which intercepts the package's calls and returns the user you define. A test with Pest or PHPUnit:
use Laravel\Socialite\Facades\Socialite;
Socialite::fake([
'github' => Socialite::userFromToken('fake-token')
->setId('12345')
->setName('Diego')
->setEmail('diego@example.com'),
]);
$response = $this->get('/auth/callback');
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', [
'provider' => 'github',
'provider_id' => '12345',
]);With this you cover the happy path, the returning user, the null email, and the failed login without depending on the network or real test accounts.
Conclusion
Social login in Laravel 13 with Socialite turns an OAuth flow that would take hundreds of lines by hand into two routes and one updateOrCreate. Install the package, configure the Google and GitHub credentials in services.php, link by provider_id, and do not forget GitHub's private email or testing with Socialite::fake(). If you want more, keep reading the blog: there are Laravel 13 web development guides every week.

