68

Slim 4 - CORS

 3 years ago
source link: https://odan.github.io/2019/11/24/slim4-cors.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Daniel’s Dev Blog

Daniel's Dev Blog

Developer, Trainer, Open Source Contributor

Blog About me Donate

Slim 4 - CORS

Daniel Opitz

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

© 2020 Daniel Opitz | Twitter


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK