Laravel SPA OAuth using GitHub, Socialite, and Sanctum
Tags: laravel, github, oauth, javascript
Published: 2023-08-31 22:03:11

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wm6rztqepza0yk8r91g3.png)

![GitHub developer settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6pr6ccnoei4hvfntc6k0.png)

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

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

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

### 2. Socialite

Install socialite:

```shell
composer require laravel/socialite
```

Add the following web route.

```php
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.

```php
use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;

Route::get('/auth/{provider}/callback', [AuthController::class, 'callback'])
    ->name('auth.callback');
```

Create the `AuthController`.

```php
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.

```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::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`.

```html
<!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.

```php
// ...

Route::get('/{path}', fn () => view('app'))
    ->where('path', '(?!api).*');
```

#### app.js

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

```js
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>).

```js
// ...

if (currentPath === '/login' || currentPath === '/register') {
    window.document.querySelector('#app').innerHTML = `
        <a href="${githubRedirectPath}">
            Login with GitHub
        </a>
    `;
}
```

![https://github.com/login/oauth/authorize?](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ik8kr54v4zp39bht31y4.png)

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

```js
// ...

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:

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)

```php
    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](https://codepen.io/login)

![Codepen login oage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jk0ydbsbgntrsx2azril.png)

---

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