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
code
to get anOAUTH-TOKEN
from GitHub. - Using the
OAUTH-TOKEN
to 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)