This is a step-by-step tutorial that requires no prior knowledge of OAuth, it just assumes that you are familiar with Laravel, and Sanctum and that you can read basic Javascript.
Overview
(TODO: sequence diagram will be added here)
Implementation
1. Configuring GitHub service
Set values in .env and /config/services.php
GITHUB_CLIENT_ID=bbb58b28cdd98636e3e2
GITHUB_CLIENT_SECRET=***************************************
GITHUB_REDIRECT=/callback
return [
// ...
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT'),
],
];
2. Socialite
Install socialite:
composer require laravel/socialite
Add the following web route.
use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;
Route::get('/auth/{provider}/redirect', [AuthController::class, 'redirect'])
->name('auth.redirect');
Add the following API route.
use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;
Route::get('/auth/{provider}/callback', [AuthController::class, 'callback'])
->name('auth.callback');
Create the AuthController.
namespace App\Http\Controllers;
use Laravel\Socialite\Facades\Socialite;
class AuthController
{
public function redirect(string $provider)
{
return Socialite::driver($provider)->stateless()->redirect();
}
public function callback(string $provider)
{
$oAuthUser = Socialite::driver($provider)->stateless()->user();
// More logic to handle login or registration will be added later
}
}
3. Storage
Edit 2014_10_12_000000_create_users_table migration:
- Make the password nullable.
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::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->timestamps();
});
}
};
4. SPA
Only one Blade file will be needed which we'll name app.blade.php.
<!DOCTYPE html>
<head>
<title>Laravel</title>
</head>
<body>
<div id="app"></div>
<script>
const githubCallbackPath = "{{ route('auth.callback', ['provider' => 'github']) }}";
const githubRedirectPath = "{{ route('auth.redirect', ['provider' => 'github']) }}";
</script>
@vite(['resources/js/app.js'])
</body>
</html>
Now we'll add a fallback web route at the end of route/web.php to serve this view.
// ...
Route::get('/{path}', fn () => view('app'))
->where('path', '(?!api).*');
app.js
We'll start by setting the current URL path value.
let currentPath = window.location.pathname;
When the current path is /login or /register we want a way to reach GitHub's authorization page.
That will be achieved via the auth.redirect route which we stored its full URL in the githubRedirectPath variable. With the help of Socialite we'll get seamlessly redirected to https://github.com/login/oauth/authorize with the needed query parameters (see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity).
// ...
if (currentPath === '/login' || currentPath === '/register') {
window.document.querySelector('#app').innerHTML = `
<a href="${githubRedirectPath}">
Login with GitHub
</a>
`;
}
The actual URL is
https://github.com/login/oauth/authorize?client_id=bbb58b28cdd98636e3e2&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2Fcallback&scope=user%3Aemail&response_type=code
After hitting the authorize button. we'll get redirected to /callback as set in GitHub and /config/services.php. The only trick is that GitHub will add a query parameter named code.
code is a one-time password that enables us to fetch an access token from GitHub (see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github). We must delegate this task to the back end since it requires additional sensitive data that is GITHUB_CLIENT_SECRET. That can be achieved by simply sending code to the back end as a query parameter at auth.callback route which we have its value in the githubCallbackPath variable.
// ...
if (currentPath === '/callback') {
let searchParams = new URLSearchParams(window.location.search);
let code = searchParams.get("code");
if (code === null) {
throw new Error("code query param must be present when entering /callback path");
}
useOauthProviderCode(code);
}
async function useOauthProviderCode(code) {
try {
const response = await fetch(`${githubCallbackPath}?code=${code}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const token = data.token;
localStorage.setItem('authToken', token);
redirectToPath('/dashboard');
} catch (error) {
console.error('Error fetching data:', error);
}
}
function redirectToPath(path) {
window.location.href = path;
}
Completing the registration/login logic
Here Socialite will abstract important operations for us:
- Using
codeto get anOAUTH-TOKENfrom GitHub. - Using the
OAUTH-TOKENto fetch user data from GitHub (see: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#3-use-the-access-token-to-access-the-api)
public function callback(string $provider)
{
$oAuthUser = Socialite::driver($provider)->stateless()->user();
$user = User::where('email', $oAuthUser->email)->first();
$user ??= User::create([
'name' => $oAuthUser->name,
'email' => $oAuthUser->email,
]);
$token = $user->createToken('token');
return ['token' => $token->plainTextToken];
}
There are more edge cases and error handling to do here, but for the sake of simplicity, this is the bare minimum that will work under ideal circumstances.
The approach of only taking the email was inspired by Codepen
Congrats 🎉. Now you can register new users, and authenticate them, and your SPA can make use of the bearer token.




Top comments (0)