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
composer require laravel/socialiteAdd the credentials to .env:
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
GITHUB_REDIRECT_URI=/auth/github/callbackThen register the provider in config/services.php. A relative redirect is resolved to a full URL automatically.
'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.
<?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
<?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
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');
});<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
httpversushttpsand the port. - InvalidStateException. This usually means the session was lost between redirect and callback, for example a different domain, a misconfigured
SESSION_DOMAINor cookies blocked onhttpwithSESSION_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
passwordis 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:
<?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.



