Home > Software design >  How to implement custom XSRF protection with Laravel and React
How to implement custom XSRF protection with Laravel and React

Time:12-06

I'm using Next.js on the frontend and Laravel as an api to create a Shopify app. I'm using Shopify's PHP API library to handle authentication (OAuth), so I'm not using Laravel's built-in auth. I just need to add XSRF protection to certain routes, such as /login, but want to check that I'm doing it safely. Here's what I've got so far:

Laravel API

  1. In app/Http/Kernal.php, I've added Laravel's session middleware to store a session for each user hitting api routes:
   protected $middlewareGroups = [
        'web' => [
            // ...
        ],

        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // I Uncommented this
            \Illuminate\Session\Middleware\StartSession::class, // I added this
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

This successfully stores a laravel_session cookie.

  1. In routes/api.php, I create a route to generate a random token, store it in the user's session, regenerate the user's session id (I believe this protects against session fixation?) and set a XSRF-TOKEN cookie for all my subdomains (httpOnly=false so can use JS to attach to header later).
// Generate xsrf token and store in user's session (laravel_session)
Route::get('/xsrf-token', function(Request $request) {
   $xsrfToken = md5(uniqid(mt_rand(), true));
   $request->session()->put('XSRF', $xsrfToken);
   $request->session()->regenerate();
   if(!$request->session()->get('XSRF')) {
        return response()->json(['message' => 'Could not set XSRF token.', 403]);
   }
   return response()->json(['message', 'success'], 200)->withCookie(
        'XSRF-TOKEN', $xsrfToken, 120, '/', config('session')['domain'], true, false
   );
});
  1. I then created a middleware function to check if the token is valid. If the token sent in the header (attached via Js in React frontend) equals the token stored in the session, the request is valid and we can continue.
public function handle(Request $request, Closure $next)
    {
        $headerXsrfToken = request()->header('x-xsrf-token');
        $sessionXsrfToken = request()->session()->get('XSRF');
        if(
            !$headerXsrfToken ||
            !$sessionXsrfToken ||
            strcmp($headerXsrfToken, $sessionXsrfToken) !== 0
        ) {
            return response()->json(['message' => 'Invalid or missing XSRF token'], 403);
        }

        $request->session()->forget('XSRF');
        $response = $next($request);
        return $response->withoutCookie('XSRF-TOKEN');
    }
  1. Create a route to test this out:
Route::get('test-xsrf', function() {
    return response('XSRF token is valid!!');
})->middleware('xsrf.check');

Next.js frontend

  1. A button calls handleValidateCSRFToken when clicked. handleValidateCSRFToken first calls /api/xsrf-token, which sets the XSRF-TOKEN cookie. We then make our request to the protected route (axios automatically attaches the XSRF-TOKEN cookie value into a x-xsrf-token header).
import axios from "axios";

const API_URL = "https://devapi.customerlift.app";

const Test = () => {
  const handleValidateCSRFToken = async () => {
    // Get csrf token cookies
    const resA = await axios.get(`${API_URL}/api/xsrf-token`, {
      withCredentials: true, // Allow cookies to be sent and set
    });
    console.log(resA);

    // Make request to xsrf-token-protected route (axios automatically sets x-xsrf-token header from the XSRF-TOKEN cookie)
    const resB = await axios.get(`${API_URL}/api/test-xsrf`, {
      withCredentials: true,
    });
    console.log(resB.data);
  };
  return (
    <button
      className="m-5 bg-blue-500 text-white p-3"
      onClick={handleValidateCSRFToken}
    >
      Validate XSRF Token
    </button>
  );
};

export default Test;

Everything seems to work well, but wanted to check if anyone can spot any issues with what I've done as this is the first time I've had to implement this by my self.

CodePudding user response:

Laravel already has a CSRF token built into the framework & request/session library accessbile via:

$request->session()->token()

or with the helper function:

csrf_token()

I don't know why you would want to generate your own.

You can add a meta tag like so:

<meta name="csrf-token" content="{{ csrf_token() }}">

And then in your JS code attach it to your requests by getting the attribute value on the meta tag.

Then just use the built in middleware: App\Http\Middleware\VerifyCsrfToken

You need to make sure you regenerate the token on login & logout, and it needs to have a reasonable expiry date.

I suggest using these methods built in the framework rather than using your own approach. It's likely going to be more secure or at minimum equally secure - and there seemly no benefit to doing your own custom thing. You don't have to be using Laravel's authentication to use the CSRF protection thats already built in.

  • Related