Laravel SPA OAuth using GitHub, Socialite, and Sanctum

medilies

medilies

Posted on August 31, 2023

Laravel SPA OAuth using GitHub, Socialite, and Sanctum

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

GitHub developer settings

GitHub developer settings

Set values in .env and /config/services.php



GITHUB_CLIENT_ID=bbb58b28cdd98636e3e2
GITHUB_CLIENT_SECRET=***************************************
GITHUB_REDIRECT=/callback


Enter fullscreen mode Exit fullscreen mode


return [
    // ...
    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect' => env('GITHUB_REDIRECT'),
    ],
];


Enter fullscreen mode Exit fullscreen mode

2. Socialite

Install socialite:



composer require laravel/socialite


Enter fullscreen mode Exit fullscreen mode

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');


Enter fullscreen mode Exit fullscreen mode

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');


Enter fullscreen mode Exit fullscreen mode

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
    }
}


Enter fullscreen mode Exit fullscreen mode

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();
        });
    }
};


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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).*');


Enter fullscreen mode Exit fullscreen mode

app.js

We'll start by setting the current URL path value.



let currentPath = window.location.pathname;


Enter fullscreen mode Exit fullscreen mode

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>
    `;
}


Enter fullscreen mode Exit fullscreen mode

https://github.com/login/oauth/authorize?

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;
}


Enter fullscreen mode Exit fullscreen mode

Completing the registration/login logic

Here Socialite will abstract important operations for us:

  1. Using code to get an OAUTH-TOKEN from GitHub.
  2. 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];
    }


Enter fullscreen mode Exit fullscreen mode

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

Codepen login oage


Congrats 🎉. Now you can register new users, and authenticate them, and your SPA can make use of the bearer token.

💖 💪 🙅 🚩
medilies
medilies

Posted on August 31, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related