How I can prevent the redirections in API using middleware on Laravel 11?

52 views Asked by At

According to this answer I need to override the default behaviour for my api calls and prevent the redirections regardless the header using a middleware:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class ApiMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        Log::info(__FUNCTION__);
        $response = $next($request);
        if ($response->getStatusCode() === 302 || $response->getStatusCode() === 301) {
            return new JsonResponse(['msg'=>"Unauthorized"], 401);
        }

        return $response;
    }
}

And upon routes/api.php using the following aproach:

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

use App\Http\Middleware\ApiMiddleware;

Route::middleware([ApiMiddleware::class])->group(function (){

    // Generating the token
    Route::put('/token',[\App\Http\Controllers\API\SaasUserController::class,'login'])
        ->name('api.login');


    Route::middleware('auth:sanctum')->group(function (){
        Route::get('/user', function (Request $request) {
            return $request->user();
        });
        // Misc routes that need authentication
    });
});

But by doing a plain GET to /api/user I get redirection according to insomnia log:


> GET /api/user HTTP/1.1
> Host: 172.161.5.2
> User-Agent: insomnia/2022.6.0
> Cookie: my_session=eyJpdiI6IjBxWVJkMlJobTREYVFuMk9wZ2xnV0E9PSIsInZhbHVlIjoiRmlRbnlwSlhBbE55a0FJS21hWE9reCs0Q1FPOW03bU1vcy9zOFhrZG1QTHlDYmgwV1VqUVgyNFJpUXE3cmxrOWI4d2lFcktDVFMydmxjZ0VmcFF3WGQvOWYvM0V4dXhQb2xya1ppNEVBNnZDMXBQNUJsaDJmV3NBMTdjdmpYREEiLCJtYWMiOiI2MGQ4MDA1ZTU1MTczYjM4NDdkNjFjOTE1ZDg3ZGU4YTUwOTlkOGRhY2EyZTJlZDU2ZTFhMWVlMWIyMjAyYWZiIiwidGFnIjoiIn0%3D; XSRF-TOKEN=xxxx
> Accept: */*

* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse

< HTTP/1.1 302 Found
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/8.2.17
< Cache-Control: no-cache, private
< Date: Thu, 28 Mar 2024 10:29:17 GMT
< Location: https://172.161.5.2/login
< Access-Control-Allow-Origin: *


* Ignoring the response-body
* Received 358 B chunk
* Connection #6 to host 172.161.5.2 left intact
* Issue another request to this URL: 'https://172.161.5.2/login'
* Found bundle for host 172.161.5.2: 0x3f5c038f58c0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#6) with host 172.161.5.2
* Connected to 172.161.5.2 (172.161.5.2) port 443 (#6)

> GET /login HTTP/1.1
> Host: 172.161.5.2
> User-Agent: insomnia/2022.6.0
> Cookie: my_session=eyJpdiI6IjBxWVJkMlJobTREYVFuMk9wZ2xnV0E9PSIsInZhbHVlIjoiRmlRbnlwSlhBbE55a0FJS21hWE9reCs0Q1FPOW03bU1vcy9zOFhrZG1QTHlDYmgwV1VqUVgyNFJpUXE3cmxrOWI4d2lFcktDVFMydmxjZ0VmcFF3WGQvOWYvM0V4dXhQb2xya1ppNEVBNnZDMXBQNUJsaDJmV3NBMTdjdmpYREEiLCJtYWMiOiI2MGQ4MDA1ZTU1MTczYjM4NDdkNjFjOTE1ZDg3ZGU4YTUwOTlkOGRhY2EyZTJlZDU2ZTFhMWVlMWIyMjAyYWZiIiwidGFnIjoiIn0%3D; XSRF-TOKEN=XXXXXX
> Accept: */*

* Mark bundle as not supporting multiuse

< HTTP/1.1 200 OK
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/8.2.17
< Cache-Control: no-cache, private
< Date: Thu, 28 Mar 2024 10:29:17 GMT

* Replaced cookie XSRF-TOKEN="XXXXXX" for domain 172.161.5.2, path /, expire 1711628957

< Set-Cookie: XSRF-TOKEN=XXXXXX; expires=Thu, 28 Mar 2024 12:29:17 GMT; Max-Age=7200; path=/; secure; samesite=lax

* Replaced cookie my_session="eyJpdiI6InBXcHQrVnc1QVVVbVBpckd1eE5VS0E9PSIsInZhbHVlIjoidEF5RGlHRTIvVFdGNnBodVdMWC9UYWYydUVzanJFOTRBcjN3WTZuYUgvSHpFempNWUZiZjVHSGJCandHdUZFakNxRFpwbGo1WGxYVWpnSjA3VlF1ZnZKZENtUWFJUENwMW9EMyt6UmpidjZWVzZTdkIrekk4d24xK0R4Wi9IeloiLCJtYWMiOiI3MzE3YmU3OGY1MTc0NTJmYjVlZTZkMDNiOWE1YTkwNGFiNGMyNmNiOWUwMThmNTFkNDg3ZWVkNGMyNGIyOGNmIiwidGFnIjoiIn0%3D" for domain 172.161.5.2, path /, expire 1711628957

< Set-Cookie: my_session=eyJpdiI6InBXcHQrVnc1QVVVbVBpckd1eE5VS0E9PSIsInZhbHVlIjoidEF5RGlHRTIvVFdGNnBodVdMWC9UYWYydUVzanJFOTRBcjN3WTZuYUgvSHpFempNWUZiZjVHSGJCandHdUZFakNxRFpwbGo1WGxYVWpnSjA3VlF1ZnZKZENtUWFJUENwMW9EMyt6UmpidjZWVzZTdkIrekk4d24xK0R4Wi9IeloiLCJtYWMiOiI3MzE3YmU3OGY1MTc0NTJmYjVlZTZkMDNiOWE1YTkwNGFiNGMyNmNiOWUwMThmNTFkNDg3ZWVkNGMyNGIyOGNmIiwidGFnIjoiIn0%3D; expires=Thu, 28 Mar 2024 12:29:17 GMT; Max-Age=7200; path=/; httponly; samesite=lax


Is there a way to override this without needing to provide accept header?

1

There are 1 answers

1
Dimitrios Desyllas On

In order to achieve this you need to apply the middleware globally at bootstrap/app.php like this:

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

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: [
            \App\Http\Middleware\ApiMiddleware::class
        ]);
        // Rest of middleware bootstrapping goes here
    })
    ->withExceptions(function (Exceptions $exceptions) {
       // Code ommited here if any
    })->create();

Then you can modify the middleware using these options as api stategies:

option 1 leave it as is

Keep the middleware as you have

option 2

Send a 401 response in a format that the server accepts as well:


namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class ApiMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        if ($response->getStatusCode() === 302 || $response->getStatusCode() === 301) {

           $accept=$request->header('Accept')??"";
           $accept=trim(preg_replace(";.*","",$accept));
           if(!empty($accept) && $accept!='application/json'){
                    $content="Unautorized";

                    switch ($accept){
                        case "application/html":
                        case "text/html":
                            $content="<!DOCTYPE html><html><head><tilte>Unautorized</tilte></head><body>Unautorized</body></html>";
                            break;
                        case 'application/xml':
                        case 'text/xml':
                            $content="<xml><msg>Unautorized</msg></xml>";
                            break;
                        default:
                            $accept="text/plain";
                    }
                   return new Response($content,401,['Content-Type',$accept]);
            }


            return new JsonResponse(['msg'=>"Unauthorized"], 401);
        }

        return $response;
    }
}


Option 3 Let the client to provide you an accept heaver as application/json

class ApiMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        if(!$request->wantsJson()){
          new Response("",400);
        }

        $response = $next($request);
        return $response;
    }
}

Option 4

As mentioned fill the nessesary header as shown in https://laraveldaily.com/tip/force-json-response-for-api-requests

Option 5 A combination of #option3 and #option4

That enforces that we only api accepts only Json.

class ApiMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        if(!$request->wantsJson()){
          new JsonResponse(['msg'=>"Invalid provided Accept Type"],400);
        }

        $request->headers->set('Accept', 'application/json');

        $response = $next($request);
        return $response;
    }
}