Implementing CORS in Zend Expressive

On a recent project, I needed to implement CORS support for my Expressive API. The easiest way to do this is to use Mike Tuupola‘s PSR-7 CORS Middleware.

As this is a standard Slim-Style PSR-7 middleware implementation, we need to wrap it for Expressive, so we make a factory:

App/Factory/CorsMiddleware.php:

<?php declare(strict_types=1);
namespace App\Factory;

use Tuupola\Middleware\Cors;
use Zend\Diactoros\Response;
use Zend\Stratigility\Middleware\CallableMiddlewareWrapper;

class CorsMiddlewareFactory
{
    public function __invoke($container)
    {
        return new CallableMiddlewareWrapper(
            new Cors([
                "origin" => ["*"],
                "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"],
                "headers.allow" => ["Content-Type", "Accept"],
                "headers.expose" => [],
                "credentials" => false,
                "cache" => 0,
            ]),
            new Response()
        );
    }
}

We then register this in our App\ConfigProvider::getDependencies() by adding to the factories key so that it looks something like this:

'factories'  => [
        Action\HomePageAction::class => Action\HomePageFactory::class,
        \Tuupola\Middleware\Cors::class => Factory\CorsMiddlewareFactory::class,
    ],

If you don’t want to fully qualify, add use Tuupola\Middleware\Cors; to the top of the file so that you can just use Cors::class here.

Lastly, we register the middleware in config/pipeline.php:

$app->pipe(\Tuupola\Middleware\Cors::class);

Place it somewhere near the top of the list; personally, I place it just after piping the ServerUrlMiddleware::class.

We now have working CORS:

$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Accept, Content-Type"  \
  -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/
HTTP/1.1 200 OK
Host: localhost:8890
Connection: close
Access-Control-Allow-Origin: http://localhost
Vary: Origin
Access-Control-Allow-Headers: content-type, accept
Content-type: text/html; charset=UTF-8

Failure looks like this:

$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Accept, Content-Type X-Clacks-Overhead"  \
  -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/
HTTP/1.1 401 Unauthorized
Host: localhost:8890
Connection: close
Content-type: text/html; charset=UTF-8

By default, it doesn’t tell you what went wrong, which isn’t too helpful.

Providing JSON error responses

To provide a JSON error response, you need to set the error option to a callable and you can then return a JsonResponse:

App/Factory/CorsMiddleware.php:

<?php declare(strict_types=1);
namespace App\Factory;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Tuupola\Middleware\Cors;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Stratigility\Middleware\CallableMiddlewareWrapper;

class CorsMiddlewareFactory
{
    public function __invoke($container)
    {
        return new CallableMiddlewareWrapper(
            new Cors([
                "origin" => ["*"],
                "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"],
                "headers.allow" => ["Content-Type", "Accept"],
                "headers.expose" => [],
                "credentials" => false,
                "cache" => 0,
                "error" => [$this, 'error'],
            ]),
            new Response()
        );
    }

    public static function error(
        RequestInterface $request,
        ResponseInterface $response,
        $arguments) {

        return new JsonResponse($arguments);
    }
}

As you can see, we’ve created a new error method that return a JsonResponse. We simply encode the $arguments as that contains the information about what the problem is:

$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Accept, Content-Type X-Clacks-Overhead"  \
  -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/
HTTP/1.1 401 Unauthorized
Host: localhost:8890
Date: Sat, 21 Oct 2017 10:41:18 +0000
Connection: close
X-Powered-By: PHP/7.1.8
Content-Type: application/json

{"message":"CORS requested header is not allowed."}

Responding with ProblemDetails

I’m a huge fan of RFC 7807, so prefer my error response to conform to this. The Problem Details component handles the creation of the correct Responses, so to use it with Cors, we change our error() method to this:

public static function error(
        RequestInterface $request,
        ResponseInterface $response,
        $arguments) {

        return self::$problemDetailsResponseFactory->createResponse(
            $request,
            401,
            '',
            $arguments['message'],
            '',
            []
        );
    }

As error() is called statically, we need to define a static property $problemDetailsResponseFactory in the class to hold our ProblemDetailsResponseFactory when we grab it from the container in the __invoke() method:

self::$problemDetailsResponseFactory = $container->get(ProblemDetailsResponseFactory::class);

You’ll need a use Zend\ProblemDetails\ProblemDetailsResponseFactory; at the top too. The entire class can be seen in this gist.

Fin

It turns out that adding CORS support to an Expressive app is easy enough, mainly because Mike Tuupola has done most of the work for us!

from Rob Allen’s DevNotes http://ift.tt/2ALRGvi
via IFTTT

Leave a Reply