Laravel Sanctum optional route authentication and guard selection
By default, Laravel Sanctum token authentication will either completely block access to protected routes or allow it at the expense of not being able to detect if the user is logged in. However, there are valid scenarios whereby a route should be accessible to guests and at the same time allow bearer token authentication. One of the many ways to achieve this is by changing the default guard assigned to the /api route group.
Let’s start by looking at an example. Imagine you have a Books route where authentication is optional. Guests can see the details of the Book, but authenticated users can also see the price of the Book. Within routes/api.php you have 2 options, either add your route inside the auth:sanctum middleware or not, like so:
// Option 1: Add it within the auth:sanctum middleware...
Route::group(['middleware' => ['auth:sanctum']], function() {
Route::post('books', [BookController::class, 'index']);
});
// ...
// Option 2: Leave it outside and lose access to the Auth facade...
Route::post('books', [BookController::class, 'index']);
This is a problem.
The reason why option 2 fails is that all routes in routes/api.php will default to the “web” guard when parsing routes outside of the “auth:sanctum” middleware. You can see it clearly in config/auth.php:
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
Unfortunately for us, the “web” guard is useless when it comes to API token authentication. We need to use instead the “sanctum” guard. However, changing config/auth.php could lead to unintended consequences if your app also consumes standard web routes. So let’s leave that as it is.
The solution
To solve this conundrum, we need to create a new middleware that allows us to switch the guard for any route group, which we can then use in our routes/api.php routes.
Start by creating a new middleware in app/Http/Middleware/SwitchGuard.php like so:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class SwitchGuard
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (in_array($guard, array_keys(config("auth.guards")))) {
config(["auth.defaults.guard" => $guard]);
}
return $next($request);
}
}
The SwitchGuard
middleware will now allow us to switch guards on-demand, without having to protect routes with the “auth” middleware.
Now we have to put our new middleware to use. Edit app/Http/Kernel.php and update it like so:
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
// your web middleware...
],
'api' => [
'guard:sanctum',
// EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
// your other middlewares...
'guard' => \App\Http\Middleware\SwitchGuard::class,
// your other middlewares...
];
}
As seen under the $middlewareGroups
array, we are forcing the “sanctum” guard on the “api” group instead of the default “web” guard that Laravel ships with.
This will allow us to call the Auth facade on routes that may or may not have authentication, like the Book example that we mentioned above.
I hope this helps and as usual, happy coding.