Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Socialite Login with GitHub Account: Step-by-Step Tutorial

๐Ÿ› ๏ธ Laravel Tutorials6 min read

Laravel 13 Socialite Login with GitHub Account: Step-by-Step Tutorial

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 Socialite Login with GitHub Account: Step-by-Step Tutorial

To add login with a GitHub account in Laravel 13, install laravel/socialite, register an OAuth app on GitHub, put its client id and secret in config/services.php, and create two routes: one that redirects to GitHub and one callback that finds or creates the local user and logs them in. Socialite handles the OAuth handshake and state check for you.

The example below adds a "Sign in with GitHub" button to an app that already has a users table and session login.

Prerequisites

  • Laravel 13 on PHP 8.3 or newer.
  • Laravel Socialite (the current major version installed by Composer).
  • A GitHub account to register the OAuth app.
  • A working session-based login, for example from a starter kit or your own controllers.

Step 1: Register an OAuth app on GitHub

In GitHub, open Settings, then Developer settings, then OAuth Apps, and create a new app. Use your app URL as the homepage and set the authorization callback URL to the exact callback route, for example http://localhost:8000/auth/github/callback. Copy the client id and generate a client secret. Create a separate OAuth app for production so local and live callbacks never mix.

Step 2: Install Socialite and add credentials

Code
composer require laravel/socialite

Add the credentials to .env:

Code
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
GITHUB_REDIRECT_URI=/auth/github/callback

Then register the provider in config/services.php. A relative redirect is resolved to a full URL automatically.

JavaScript
'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect' => env('GITHUB_REDIRECT_URI', '/auth/github/callback'),
],

Step 3: Add GitHub columns to users

Store the GitHub user id so you can match returning users even if they change their email. Make the password nullable because social users may never set one.

JavaScript
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('github_id')->nullable()->unique()->after('id');
            $table->string('avatar')->nullable();
            $table->string('password')->nullable()->change();
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['github_id', 'avatar']);
        });
    }
};

Add github_id and avatar to the model's fillable attributes. I do not store the GitHub access token here because a plain login does not need it. If you later call the GitHub API on the user's behalf, store the token in an encrypted cast column.

Step 4: Create the controller

JavaScript
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Socialite;
use Throwable;

class GitHubLoginController extends Controller
{
    public function redirect(): RedirectResponse
    {
        return Socialite::driver('github')
            ->scopes(['read:user', 'user:email'])
            ->redirect();
    }

    public function callback(): RedirectResponse
    {
        try {
            $githubUser = Socialite::driver('github')->user();
        } catch (Throwable $e) {
            return redirect()->route('login')->withErrors(['email' => 'GitHub login failed. Please try again.']);
        }

        if (! $githubUser->getEmail()) {
            return redirect()->route('login')->withErrors(['email' => 'Your GitHub account has no verified email we can use.']);
        }

        $user = User::where('github_id', $githubUser->getId())->first()
            ?? User::where('email', $githubUser->getEmail())->first();

        if ($user) {
            $user->update([
                'github_id' => $githubUser->getId(),
                'avatar' => $githubUser->getAvatar(),
            ]);
        } else {
            $user = User::create([
                'name' => $githubUser->getName() ?? $githubUser->getNickname(),
                'email' => $githubUser->getEmail(),
                'github_id' => $githubUser->getId(),
                'avatar' => $githubUser->getAvatar(),
            ]);
        }

        Auth::login($user, true);
        request()->session()->regenerate();

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

The user:email scope lets Socialite read the primary email even when the user hides it on their public profile. Some accounts still return no usable email, so the controller stops with a clear message instead of creating a broken user.

Is linking by email safe?

Linking an existing account by email is convenient, but it trusts GitHub's email verification. That is reasonable for GitHub, which only exposes verified primary emails through this scope. For stricter apps, link GitHub only from a logged-in settings page and match new logins by github_id alone.

Step 5: Register the routes and button

CSS
use App\Http\Controllers\Auth\GitHubLoginController;

Route::middleware('guest')->group(function () {
    Route::get('/auth/github/redirect', [GitHubLoginController::class, 'redirect'])->name('github.redirect');
    Route::get('/auth/github/callback', [GitHubLoginController::class, 'callback'])->name('github.callback');
});
JavaScript
<a href="{{ route('github.redirect') }}" class="btn">Sign in with GitHub</a>

Common mistakes

  • Callback URL mismatch. GitHub rejects the request if the redirect in your config differs from the one in the OAuth app, including http versus https and the port.
  • InvalidStateException. This usually means the session was lost between redirect and callback, for example a different domain, a misconfigured SESSION_DOMAIN or cookies blocked on http with SESSION_SECURE_COOKIE=true.
  • Using stateless() in a web app. It disables the state check. Keep it for token-based APIs only.
  • Required password column. Inserting a social user fails if password is still NOT NULL.

How do you verify it works?

Click the button, approve the app on GitHub and confirm you land on the dashboard with a new row in users that has github_id set. Then add an automated test with Socialite's fake, which avoids real HTTP calls:

JavaScript
<?php

use Laravel\Socialite\Socialite;
use Laravel\Socialite\Two\User as SocialiteUser;

test('a user can sign in with github', function () {
    Socialite::fake('github', SocialiteUser::fake([
        'id' => 'gh-1001',
        'name' => 'Test Dev',
        'nickname' => 'testdev',
        'email' => 'dev@example.com',
    ]));

    $this->get('/auth/github/callback')->assertRedirect('/dashboard');

    $this->assertDatabaseHas('users', ['email' => 'dev@example.com', 'github_id' => 'gh-1001']);
    $this->assertAuthenticated();
});

What data does GitHub return?

Socialite gives you a user object with getId(), getNickname() (the GitHub username), getName(), getEmail() and getAvatar(). The name can be empty because many GitHub users never set one, which is why the controller falls back to the nickname. The id is the only value that never changes, so it is the right key for matching accounts. Usernames and emails can both be changed by the user on GitHub.

If you also offer email and password login, show a "Connected accounts" section in the profile page so users can see that GitHub is linked and disconnect it after setting a password.

FAQ

Can I use GitHub login for an API or mobile app?

Yes. Use stateless() or userFromToken(), then issue your own API token, for example with Sanctum.

Do I need a separate OAuth app per environment?

It is the simplest option. Each GitHub OAuth app has one callback URL, so separate apps keep local and production clean.

How do I request access to repositories?

Add scopes such as public_repo with scopes(). Ask only for what you use, because users see every scope on the consent screen.

References

Article Index & Topic Tags
LaravelSocialiteAuthenticationOauthGithub

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.