Adding Cross Origin Resource Sharing (CORS) Support In Your Laravel PHP Application

Adding Cross Origin Resource Sharing (CORS) support to your Laravel app is pretty straightforward and in this article, we’ll take a look at how to do that using Laravel middlewares. If you’re reading this post then you already know about CORS which is a solution for cross origin XHR or Fetch requests. Incase you want to get an in-depth knowledge on this topic, read these:

Create Cors Middleware

If you’re using the Laravel PHP framework, then enabling CORS is super simple. The best way to do this is by using a middleware which encapsulates and separates the code completely. Let’s create a middleware called Cors in our Laravel project:

$ php artisan make:middleware Cors
Middleware created successfully.

Middleware Code

Now just open the file located at app/Http/Middleware/Cors.php and paste the following snippet (we’ll dissect and examine the different sections in a bit):

<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
    private static $allowedOriginsWhitelist = [
      'http://localhost:8000'
    ];

    // All the headers must be a string

    private static $allowedOrigin = '*';

    private static $allowedMethods = 'OPTIONS, GET, POST, PUT, PATCH, DELETE';

    private static $allowCredentials = 'true';

    private static $allowedHeaders = '';

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
      if (! $this->isCorsRequest($request))
      {
        return $next($request);
      }

      static::$allowedOrigin = $this->resolveAllowedOrigin($request);

      static::$allowedHeaders = $this->resolveAllowedHeaders($request);

      $headers = [
        'Access-Control-Allow-Origin'       => static::$allowedOrigin,
        'Access-Control-Allow-Methods'      => static::$allowedMethods,
        'Access-Control-Allow-Headers'      => static::$allowedHeaders,
        'Access-Control-Allow-Credentials'  => static::$allowCredentials,
      ];

      // For preflighted requests
      if ($request->getMethod() === 'OPTIONS')
      {
        return response('', 200)->withHeaders($headers);
      }

      $response = $next($request)->withHeaders($headers);

      return $response;
    }

    /**
     * Incoming request is a CORS request if the Origin
     * header is set and Origin !== Host
     *
     * @param  \Illuminate\Http\Request  $request
     */
    private function isCorsRequest($request)
    {
      $requestHasOrigin = $request->headers->has('Origin');

      if ($requestHasOrigin)
      {
        $origin = $request->headers->get('Origin');

        $host = $request->getSchemeAndHttpHost();

        if ($origin !== $host)
        {
          return true;
        }
      }

      return false;
    }

    /**
     * Dynamic resolution of allowed origin since we can't
     * pass multiple domains to the header. The appropriate
     * domain is set in the Access-Control-Allow-Origin header
     * only if it is present in the whitelist.
     *
     * @param  \Illuminate\Http\Request  $request
     */
    private function resolveAllowedOrigin($request)
    {
      $allowedOrigin = static::$allowedOrigin;

      // If origin is in our $allowedOriginsWhitelist
      // then we send that in Access-Control-Allow-Origin

      $origin = $request->headers->get('Origin');

      if (in_array($origin, static::$allowedOriginsWhitelist))
      {
        $allowedOrigin = $origin;
      }

      return $allowedOrigin;
    }

    /**
     * Take the incoming client request headers
     * and return. Will be used to pass in Access-Control-Allow-Headers
     *
     * @param  \Illuminate\Http\Request  $request
     */
    private function resolveAllowedHeaders($request)
    {
      $allowedHeaders = $request->headers->get('Access-Control-Request-Headers');

      return $allowedHeaders;
    }
}

Sample Routes (Test App)

As you’ll see the code is pretty straightforward. To test this all I did was create a new Laravel project and served them on two different ports:

$ php artisan serve --port 8000
Laravel development server started: <http://127.0.0.1:8000>

# and

$ php artisan serve --port 8080
Laravel development server started: <http://127.0.0.1:8080>

Since the ports are different they are treated as different origins by our clients (browsers). What you can do now is created two different endpoints (in order to test our CORS mechanism):

// routes/web.php

Route::get('/one', function () {
  return view('one'); // resources/views/one.blade.php
});

Route::put('/two', function () {
  return view('two'); // resources/views/two.blade.php
});

Now if the client makes and XHR PUT request from localhost:8000/one to localhost:8080/two it should go through right ? Not really. We’ve created the middleware class but we also need to enable it.

Middleware Configuration

There are multiple ways to do enable a middleware that executes before/after our requests. All these ways will require changes to app/Http/Kernel.php. The easiest option is to open app/Http/Kernel.php in your favourite editor and add the Cors middleware class to the $middleware instance variable. The new variable block should look something like this:

protected $middleware = [
    // Other middleware classes ...
    \App\Http\Middleware\Cors::class,
];

These middlewares run before every request in the entire laravel application.

There might be cases where you’d selectively want to enable Cors for specific route groups and not for all the routes/requests across the app. In such cases you can put the Cors class entry in the $middlewareGroups (before the VerifyCsrfToken middleware) or $routeMiddleware variables in the same Kernel class.

Although there is a problem with this approach. The middlewares defined in $middlewareGroups and $routeMiddleware variables run after the routes are resolved by the framework. This is fine for cross origin GET and POST requests but in other cases (PUT, DELETE, etc.) which are preceded by an OPTIONS requests (also known as preflighted requests), we’ll have troubles since the middleware will be executed after route resolution for the OPTIONS method. Internally, Laravel responds to OPTION by default if it is unable to find a Route::options(...) definition in our routes file. This happens before the $middlewareGroups or $routeMiddleware middlewares are executed.

What this means is that when the client sends an OPTION request for preflighted requests, our middleware won’t be hit and the Access-Control-Allow-* headers won’t be passed down to the client that’ll prevent the subsequent PUT or DELETE or whatever request. So in order to get around this we’ll have to define the same route handler for OPTIONS as well. Code-wise, what I’m basically saying is this:

// Note how the same route has been defined on options as well as put
//
// Now since Laravel will find a route mapped to `OPTIONS /two` internally,
// it'll execute our middleware first and then this route.
Route::match(['options', 'put'], '/two', function () {

  return view('two');

})->middleware('cors');

The ->middleware('cors') is required if you use the $routeMiddleware variable. It’s not required if you for instance put this in the web group of $middlewareGroups and obviously the route is also defined in routes/web.php as the middleware in that case is automatically applied in app/Providers/RouteServiceProvider.php:

protected function mapWebRoutes()
{
    Route::middleware('web')
         ->namespace($this->namespace)
         ->group(base_path('routes/web.php'));
}

Understanding Middleware Code

Now that the entire process is understood and maybe you even got things working, let’s quickly break down our middleware code and understand it.

public function handle($request, Closure $next)
{
  if (! $this->isCorsRequest($request))
  {
    return $next($request);
  }

  static::$allowedOrigin = $this->resolveAllowedOrigin($request);

  static::$allowedHeaders = $this->resolveAllowedHeaders($request);

  $headers = [
    'Access-Control-Allow-Origin'       => static::$allowedOrigin,
    'Access-Control-Allow-Methods'      => static::$allowedMethods,
    'Access-Control-Allow-Headers'      => static::$allowedHeaders,
    'Access-Control-Allow-Credentials'  => static::$allowCredentials,
  ];

  if ($request->getMethod() === 'OPTIONS')
  {
    return response('', 200)->withHeaders($headers);
  }

  $response = $next($request)->withHeaders($headers);

  return $response;
}

As you can see what the handle() method does initially is, if the request is not a CORS request it just executes the next middleware/route and returns right there. This means even if this middleware is executed for all the routes in our app, we should be fine as our same origin requests will be unaffected and continue to work as expected. The way to check whether the current request is a CORS request or not is simple:

private function isCorsRequest($request)
{
  $requestHasOrigin = $request->headers->has('Origin');

  if ($requestHasOrigin)
  {
    $origin = $request->headers->get('Origin');

    $host = $request->getSchemeAndHttpHost();

    if ($origin !== $host)
    {
      return true;
    }
  }

  return false;
}

The Origin header must be set and it shouldn’t be the same as the Host of the incoming request.

Next up inside the the handle() method only, if the request is a cross origin one then we resolve an origin by checking against a whitelist to pass in Access-Control-Allow-Origin. This is important if we wish to not pass the default value set in the code (*) which enables cross origin access for the entire web. This is also important if we wish to use Access-Control-Allow-Credentials: true.

Finally we return the response with the appropriate headers but you’ll notice this piece in between for preflighted requests:

// For preflighted requests
if ($request->getMethod() === 'OPTIONS')
{
  return response('', 200)->withHeaders($headers);
}

Guess this is obvious as well. The client will send an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers (optional) which will be used to see if the server communication can happen with the expected request method and request headers. So for instance, in a PUT request the client sends an OPTIONS request with Access-Control-Request-Method: PUT header and if the server doesn’t respond with PUT as a value in Access-Control-Allow-Methods then the client won’t really send the actual PUT request subsequently. We also choose to not send any response body as that is not required. Headers are the important pieces for the client browser to determine whether the target resource should be fetched with the actual request methods and headers.

Code Improvements

I just wanted to point out that you could take a step further and improve the code a little by moving the configuration data to an actual config file. This is the data I’m referring to:

private static $allowedOriginsWhitelist = [
  'http://localhost:8000'
];

// All the headers must be a string

private static $allowedOrigin = '*';

private static $allowedMethods = 'OPTIONS, GET, POST, PUT, PATCH, DELETE';

private static $allowCredentials = 'true';

private static $allowedHeaders = '';

We can put these configuration pieces in a file called config/cors.php and then fetch that in the constructor method of the middleware using the config('cors') global helper. This is completely up to you!

Wrapping Up

With Laravel middlewares it’s super simple to enable CORS in your PHP application. You can either enable it at a “global” level or at a local level using middleware in route groups.

I’ve put together a sample Laravel app on Github with the Cors middleware for folks interested in quick testing. Hope that helps!

Leave a Reply

Your email address will not be published. Required fields are marked *