night_driver's avatar

Sanctum override JSON Response (AuthenticationException)

Hello everyone,

I'm currently using Laravel 11 and I use sanctum for authentication.

When i don't pass any Bearer Token i have the redirection to the Login page through AuthenticationException, that's the normal workflow from sanctum.

As my Laravel project will be only for API i would like to return a custom Json response. I know that i can pass the "Accept : application/json" in the Header request to return the default sanctum error Json but i would like to always return my custom JSON without passing "Accept : application/json" in the Header.

My question : How can I override the default workflow when AuthenticationException is raised by Sanctum ? I tried by creation a custom Exception and also in the bootstrap/app.php but nothing works.

bootstrap/app.php ->

use Illuminate\Auth\AuthenticationException;

    ->withExceptions(function (Exceptions $exceptions) {

       $exceptions->stopIgnoring(AuthenticationException::class);

        $exceptions->render(function (AuthenticationException $exception, Request $request) {
            
            //Unauthorized
            return response()->json([
                'message' => 'Unauthorized action',
			    'custom_field' => 'My new field'
            ], 401);
        });

Thank very much !

1 like
8 replies
night_driver's avatar

I found the solution.

I posted here if someone need it.

The problem was that without passing the Accept: application/json in the header Sanctum tried to redirect to the /login route that i didn't set. Because of this, laravel throw a NotRouteFound Exception so I could not catch the AuthentificationException.

Here is the solution

  1. Add a middleware with " Accept:application/json" for all incoming request because i use laravel for API only . In this way Sanctum will not try to redirect me to the Login page. This will also helps for other aspect of the application.
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ForceJsonRequestHeader
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}
  1. Register this middleware to the application in the boostrap/app.php
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(ForceJsonRequestHeader::class);
    })
  1. Modify the rendering of the AutenticationException and add my custom fields
        $exceptions->render(function (AuthenticationException $e, Request $request) {

            return response()->json([
                'message' => 'Not Authorized',
                'my_custom' => 'custom_field'
            ], 401);
        });

Here the complete bootstrap/app.php

<?php

use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

//Middleware
use App\Http\Middleware\ForceJsonRequestHeader;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {

        //Force json for all incoming requests
        $middleware->append(ForceJsonRequestHeader::class);
    })
    ->withExceptions(function (Exceptions $exceptions) {

        //Custom Rendering
        $exceptions->render(function (AuthenticationException $e, Request $request) {

            return response()->json([
                'message' => 'Not Authorized',
                'my_custom' => 'custom_field'
            ], 401);
        });

    })->create();
3 likes
Maelfjord's avatar

I also ran into this problem earlier and since I have some endpoints that doesn't need to return JSON, I have set a middleware alias to the force JSON middleware and just put it to the endpoints that need it.

I'll add that If you don't want to create a custom Authentication Exception, I have discovered that middlewares like auth take priority over the ones that is user created. So regardless of whether you put it first on the list of middlewares (Route::middleware('force-json', 'auth:student,admin')), auth will run first, resulting in a login route search and possibly a webpage response which is not what you might want if you prefer JSON response and a status code.

To fix this, you can modify the middleware priority in boostrap/app.php using $middleware->priority([]); and set the force JSON middleware to be of top priority so it gets executed first:

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

use App\Http\Middleware\ForceJsonMiddleware;
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'force-json' => ForceJsonMiddleware::class,
            'ablilites' => CheckAbilities::class,
            'ability' => CheckForAnyAbility::class
        ]);

        $middleware->priority([
            ForceJsonMiddleware::class
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

This way, you can be sure that the force JSON middleware is executed first when you include it as a middleware:

Route::middleware(['force-json', 'auth:student,admin'])->group(function () {});
shahzaibtariqbutt's avatar

If anyone working with both api and web routes and he only wants to force (Accept: application/json) header for api routes then he can do something like this in bootstrap/ap.php:

return Application::configure(basePath: dirname(DIR))
    ->withRouting(
        web: DIR.'/../routes/web.php',
        api: DIR.'/../routes/api.php',
        commands: DIR.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->api(prepend: ForceJsonRequestHeader::class);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        Integration::handles($exceptions);
    })->create();

this is just to prepend another middleware i.e. ForceJsonRequestHeader brfore any other middlewares. And ForceJsonRequestHeader is to force header as mentioned above in the thread.

Please or to participate in this conversation.