DEV Community

Unpublished Post. This URL is public but secret, so share at your own discretion.

Laravel Sanctum Explained : SPA Authentication on a different domain

In the last part we saw how to authenticate a Single Page App using the stateful (using cookies and sessions) authentication of Laravel Sanctum. This is really the recommended way of dealing with authentication on an SPA and you should try to stick with it as much as possible. But it has one major prerequisite, which is that both the API and the SPA should be on subdomains from the same top level domain (somthing like www.myapp.com and api.myapp.com), this is because you can't share cookies between completely unrelated domains.

If you have to use completely unrelated domains for your API and your front-end, you will have to use stateless authentication.

Let's assume our back-end is hosted on the sanctum-api.test domain, and the front-ends on completely different domains like myapp.com.

Setting up stateless authentication on the backend

First thing is to setup our backend so it can issue tokens. These tokens will have to be given out to each user when they enter their email and password on the front-end, so we should have a route that accepts an email and a password, checks that they match a user, generate a token for this user and return it.

Let's put it in an App\Http\Controllers\AuthController, and it will look like this :

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function getSanctumTokenFromCredentials(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required'
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        return $user->createToken('frontend')->plainTextToken;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is almost a copy/paste of the Laravel documentation regarding Mobile App authentication, except we don't ask for a device name.

Then add a route in resources/routes/api.php :

<?php

use App\Http\Controllers\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/sanctum/token', [AuthController::class, 'getSanctumTokenFromCredentials']);
Enter fullscreen mode Exit fullscreen mode

From now we should be able to POST an email and password on this route and get a new token in return.

Protecting backend routes with Sanctum

Now we'll want to protect the rest of our routes to make sure only authenticated users can access them. In a real project we'd create a group and probably add scopes, but for now we'll keep it really simple and says that any user who has a valid token can look at their own profile by using the auth:sanctum middleware :

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});
Enter fullscreen mode Exit fullscreen mode

So now we have URLs assuming our back-end is hosted on the domain sanctum-api.test :

And we can POST on the first one to retrieve a token, and then GET the second one with the token to retrieve data. The token should be passed as a Bearer token in the Authorization header, this means if the first route returned the token 18|NYM99DPm9QtFyGhV3ZBDCLe6Xnga2wD4kGi7IA0W, all your subsequent queries should have these headers similar to this:

Accept: application/json
Content-Type: application/json
Authorization: Bearer 18|NYM99DPm9QtFyGhV3ZBDCLe6Xnga2wD4kGi7IA0W
Enter fullscreen mode Exit fullscreen mode

Retrieving data from the front-end

From another Laravel App

Let's say our frontend itself is another Laravel app that uses the Http object to make requests to the back-end. What we'd need to do is :

  • Ask the user for their email and password
  • Send them to the back-end
  • Retrieve the token and save it (in the user session)
  • Insert the token in every following query to the backend.

Let's make a really simple app that asks for the credentials and then displays the full User object.

So we'll need to point '/' to a login form in our /routes/web.php file :

<?php

use Illuminate\Support\Facades\Route;
use \Illuminate\Http\Request;

Route::get('/', function () {
    return view('login');
});
Enter fullscreen mode Exit fullscreen mode

And then make a quick and dirty form in resources/views/login.blade.php :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel Sanctum Front Example</title>
</head>
<body>
<form action="{{route('login')}}" method="post">
    @csrf
    <label>Login : <input name="email" type="text" /></label>
    <label>Password : <input name="password" type="text" /></label>
    <button type="submit">LOGIN</button>
</form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

So the form will POST the email and password (to our front-end) that will need to send them to our api, and retrieve the token that we'll store in a 'sanctum_token' element of our session. This is pretty easy with the Http Facade, we use the acceptJson() method to insert the Accept: application/json header and tell our backend that we want the answer formatted as a json :

Route::post('/login', function (Request $request) {
    $response = Http::acceptJson()->post('https://sanctum-api.test/api/sanctum/token', [
        'email' => $request->input('email'),
        'password' => $request->input('password')
    ]);

    if ($response->successful()) {
        session(['sanctum_token' => $response->body()]);
        return redirect()->route('home');
    }

    return redirect()->back();
})->name('login');
Enter fullscreen mode Exit fullscreen mode

If everything goes right, we get the token, store it and redirects the user to the 'home' route. As an example let's make the home route display the full User object from the api. We'll use acceptJson() as before, and the withToken() method that will insert the Authorization: Bearer $token header :

Route::get('/home', function (Request $request) {

    $response = Http::acceptJson()
        ->withToken(session('sanctum_token'))
        ->get('https://sanctum-api.test/api/user');

    dd($response->json());
})->name('home');
Enter fullscreen mode Exit fullscreen mode

And that's it really ! You should now be able to retrieve the user object from the back-end and display it in your front-end.

Note that in this example the tokens issued by the back-end will never expire, but the session on the front-end will, so the user will have to log back in when their session expires. You could probably set a (pretty long) expiration date on the back-end tokens and purge them from time to time.

Top comments (0)