Slim 4 - CORS
Daniel Opitz
24 Nov 2019
When you implement your first web application (SPA) and deploy the API endpoints on a different hostname (domain), your browser should complain about CORS security policies.
The error message might be something like this:
Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
It does not have HTTP ok status.
To allow the browser to access a foreign domain, the API must provide the client
with the correct HTTP headers in the response. Before the browser sends the real
request a preflight request is sent to the same URL using the http OPTIONS
method.
The API must answer this options
request with the status code 200
.
This flowchart describes it well: https://gluu.org/docs/ce/admin-guide/cors/.
Middleware
To send the correct CORS header you first need the following middleware:
File: src/Middleware/CorsMiddleware.php
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;
/**
* CORS middleware.
*/
final class CorsMiddleware implements MiddlewareInterface
{
/**
* Invoke middleware.
*
* @param ServerRequestInterface $request The request
* @param RequestHandlerInterface $handler The handler
*
* @return ResponseInterface The response
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$routeContext = RouteContext::fromRequest($request);
$routingResults = $routeContext->getRoutingResults();
$methods = $routingResults->getAllowedMethods();
$requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
$response = $handler->handle($request);
$response = $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', implode(', ', $methods))
->withHeader('Access-Control-Allow-Headers', $requestHeaders ?: '*');
// Optional: Allow Ajax CORS requests with Authorization header
$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
return $response;
}
}
Add the CorsMiddlware
before the RoutingMiddleware
.
// ...
$app->add(\App\Middleware\CorsMiddleware::class); // <--- here
// The RoutingMiddleware should be added after our CORS middleware
// so routing is performed first
$app->addRoutingMiddleware();
// ...
Routing
An OPTIONS
(preflight) request is basically a route like any other
and must be added for each route (path) you want to allow.
Example 1: The route /
$app->get('/', \App\Action\HomeAction::class);
// Allow preflight requests for /
$app->options('/', function (
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
return $response;
});
Example 2: The route /example
$app->post('/example', \App\Action\ExampleAction::class);
// Allow preflight requests for /example
$app->options('/example', function (
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
return $response;
});
Example for a route group:
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Routing\RouteCollectorProxy;
// ...
$app->group('/v2', function (RouteCollectorProxy $group) {
// Authentication
$group->post('/login', \App\Action\Authentication\LoginAction::class);
// Allow preflight requests for /v2/login
$group->options('/login', function (
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
return $response;
});
$group->post('/register', \App\Action\Authentication\RegisterAction::class);
// Allow preflight requests for /v2/register
$group->options('/register', function (
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
return $response;
});
// and so on...
});
To reduce the routing boilerplate code you could add a simple PreflightAction
class.
File: src/Action/PreflightAction.php
<?php
namespace App\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class PreflightAction
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
// Do nothing here. Just return the response.
return $response;
}
}
Here is an complete routing example with (options) preflight routes:
use App\Action\PreflightAction;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Routing\RouteCollectorProxy;
//
// The routes
//
$app->group('/api/v0', function (RouteCollectorProxy $group) {
$group->get('/users', function (
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
$response->getBody()->write('List all users');
return $response;
});
$group->post('/users', function (
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
// Retrieve the JSON data
$data = (array)$request->getParsedBody();
// Your code here
$response->getBody()->write('Create user');
return $response;
});
// Allow preflight requests for /api/v0/users
// Due to the behaviour of browsers when sending a request,
// you must add the OPTIONS method.
$group->options('/users', PreflightAction::class);
$group->get('/users/{id}', function (
ServerRequestInterface $request,
ResponseInterface $response,
array $args
): ResponseInterface {
$userId = (int)$args['id'];
$response->getBody()->write(sprintf('Get user: %s', $userId));
return $response;
});
$group->delete('/users/{id}', function (
ServerRequestInterface $request,
Response $response, array $arguments
): Response {
$userId = (int)$arguments['id'];
$response->getBody()->write(sprintf('Delete user: %s', $userId));
return $response;
});
$group->put('/users/{id}', function (
ServerRequestInterface $request,
Response $response,
array $arguments
): Response {
// Your code here...
$userId = (int)$arguments['id'];
$response->getBody()->write(sprintf('Put user: %s', $userId));
return $response;
});
$group->patch('/users/{id}', function (
ServerRequestInterface $request,
Response $response,
array $arguments
): Response {
$userId = (int)$arguments['id'];
$response->getBody()->write(sprintf('Patch user: %s', $userId));
return $response;
});
// Allow preflight requests for /api/v0/users/{id}
$group->options('/users/{id}', PreflightAction::class);
});
$app->run();
Test script
<html>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script>
$.ajax({
// change the url here
url: "http://localhost/api/v0/users",
type: "GET",
// Add the Authorization header if needed
headers: {'Authorization' : 'Bearer 12345'},
cache: false,
// contentType: 'application/json',
dataType: 'json'
}).done(function (data) {
alert('Successfully');
console.log(data);
}).fail(function (xhr) {
var message = 'Server error';
if (xhr.responseJSON && xhr.responseJSON.error.message) {
message = xhr.responseJSON.error.message;
}
alert(message);
});
</script>
</html>
Read more: Setting up CORS