Slim v4 - CORS tuupola

Hi,
I have an API written in PHP (7.4.0) following the very nice and useful tutorial.

Everything works really well as long as I use Postman for my requests. I have to say that I am still struggling to force Slim to emit errors in JSON format. I absolutely do not need HTML as Slim is used only as a RESTful API… I find it really difficult to make this possible as it should be a simple feature. But that is not my question here.

I have built a web application that is trying to authenticate the users on the API which sends a JWT token back when the authentication is successful. Again, that works like a breeze in Postman… But as soon as I try with a browser (Chrome in my case) I struggle with CORS issues… I did setup the tuupola/cors-middleware package and I read an followed most of the tutorial I could find but nothing seems to work.
I am convinced that there is something wrong in my code which is very much dependent on the code found in the tutorial by @odan (Absolutely nothing wrong with the tutorial but rather with my understanding of it)

So, here is the question: How to get rid of that CORS issue?

Access to XMLHttpRequest at 'http://localhost:8000/v2/atzn/login' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

My routes.php (only part of it here) looks like:

<?php

use Slim\App;

return function (App $app) {


    $app->get('/', \App\Action\HomeAction::class);
    
    $app->group('/v2', function ($app) {
        // Authentication
        $app->group('/atzn', function ($app) {
            $app->post('/login', \App\Action\Authentication\LoginAction::class);
            $app->post('/register', \App\Action\Authentication\RegisterAction::class);
        });

I have a cors.php used as a middleware looking like:

<?php 
use Slim\App;
use Tuupola\Middleware\CorsMiddleware;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;


return function (App $app) {
    $logger = new Logger("CORS");
$rotating = new RotatingFileHandler(__DIR__ . "/../logs/slim.log", 0, Logger::DEBUG);
$logger->pushHandler($rotating);
    $app->add(new Tuupola\Middleware\CorsMiddleware([
        "logger" => $logger,
        "origin" => ["*"],
        "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"],
        "headers.allow" => ["Authorization", "If-Match", "If-Unmodified-Since"],
        "headers.expose" => [],
        "credentials" => true,
        "cache" => 0
    ]));

};

Could someone help me?
Thanks a million in advance.

Pierre

Hi! I am on holiday an can’t write a complete answer from here. Postman doesn’t care about CORS, like Chrome and Firefox. Make sure that you setup the routes for OPTION request and read more about CORS prefligth requests.

Have you consulted these resources?

1 Like

Enjoy your holidays to the fullest and forget about I.T. stuff.
Thanks for the links. I’ll dig into that.

Did you get any further? Did it work?

Hi @odan,
Unfortunately not.
I tried to figure out where the issue is.
I now see the correct headers in Postman (the response from my API) but angular still triggers an error about CORS :frowning:

I don’t know the tuupola CORS library. But you could ask the authors of the (tuupola) library here:

Hi,
My previous message lacked some details.
I actually couldn’t use the tuupola CORS libray. I tried but I couldn’t even get the proper headers in my response. So I gave up with the tuupola CORS library and went through the links you recommended.

Especially this one

My API directory structure is pretty much the one I found in your PDO related tutorial.

I am now struggling to find where to integrate the CORS code into the API code.

Now my middleware.php looks like this:

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
use Slim\Routing\RouteContext;
use Selective\Config\Configuration;
use Slim\App;

return function (App $app) {
    // Parse json, form data and xml
    $app->addBodyParsingMiddleware();

    $app->add(function (Request $request, RequestHandlerInterface $handler): Response {
        $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', '*');
        $response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods));
        $response = $response->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 routing middleware
    $app->addRoutingMiddleware();

    $container = $app->getContainer();
    
    // Add error handler middleware
    $settings = $container->get(Configuration::class)->getArray('error_handler_middleware');
    $displayErrorDetails = (bool)$settings['display_error_details'];
    $logErrors = (bool)$settings['log_errors'];
    $logErrorDetails = (bool)$settings['log_error_details'];

    $customErrorHandler = function (
        ServerRequestInterface $request,
        Throwable $exception,
        bool $displayErrorDetails,
        bool $logErrors,
        bool $logErrorDetails
    ) use ($app) {
        $payload = ['error' => $exception->getMessage()];
    
        $response = $app->getResponseFactory()->createResponse();
        $response->getBody()->write(
            json_encode($payload, JSON_UNESCAPED_UNICODE)
        );
    
        return $response;
    };

    $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails);
    //$errorMiddleware->setDefaultErrorHandler($customErrorHandler);


    

};

And I am now trying to insert the correct code in the routes.php file but I do not really understand where to put the code to make it work with several URL groups.

So this is where I am at the moment knowing that I receive in Postman the headers.

Here is ab abstract of my routes.php.
I have to say that I don’t really know where to place the OPTIONS.

<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;

return function (App $app) {


    $app->get('/', \App\Action\HomeAction::class);

    $app->options('', function (Request $request, Response $response): Response {
        return $response;
    });
    
    $app->group('/v2', function ($app) {
        // Authentication
        $app->group('/atzn', function ($app) {
            $app->post('/login', \App\Action\Authentication\LoginAction::class);
            $app->post('/register', \App\Action\Authentication\RegisterAction::class);
            $app->options('/login', function (Request $request, Response $response): Response {
                return $response;
            });
        });

I am currently debugging some code on the Angular side just to make sure the issue is not there.

Thanks again for your help and support.

An options request is basically a route like any other. The options routes (for the preflight requests) must be added for each route (path) you want to allow.

For example the route /

$app->get('/', \App\Action\HomeAction::class);

$app->options('/', function (Request $request, Response $response): Response {
    return $response;
});

Example 2, the route /example

$app->post('/example', \App\Action\ExampleAction::class);

$app->options('/example', function (Request $request, Response $response): Response {
    return $response;
});

Example for a route group:

use Slim\Routing\RouteCollectorProxy;

// ...

$app->group('/v2', function (RouteCollectorProxy $group) {
    // Authentication
    $group->group('/atzn', function (RouteCollectorProxy $group) {
        $group->post('/login', \App\Action\Authentication\LoginAction::class);

        // Allow preflight requests for /v2/atzn/login
        $group->options('/login', function (Request $request, Response $response): Response {
            return $response;
        });

        $group->post('/register', \App\Action\Authentication\RegisterAction::class);

        // Allow preflight requests for /v2/atzn/register
        $group->options('/register', function (Request $request, Response $response): Response {
            return $response;
        });

        // ...

});

I would try this approach first. But you could also try the official Slim 4 approach by defining a “catch all” route for all options requests. It’s never worked for me so far because I’ve got some strange unexpected routing behaviors and errors.

Edit: I just updated the CORS tutorial.

Hi,
maybe I miss something, but in case I have mix of API and template in my app, and I do not have any issue with CORS, and I do not use option routes. But in my case Il’m storing auth data in the session, maybe it’s the case

Also you mentioned that you use PHP 7.4, is Slim and most packages fully supporting it? i really want to use it but wasnot sure if it make sense to use it already

@bogdan_dubyk

I do not have any issue with CORS,

In this case you are running everything on the same host (domain). Some people are using different hosts (domains) for the App and the API.

Also you mentioned that you use PHP 7.4, is Slim and most packages fully supporting it?

CORS is a client (browser) related issue. The server just has to send the correct response headers and handle the preflight OPTIONS requests, which is no problem for PHP in general.

Well, I have frontend vue app which is in separate server and has different domain, which is using same backend alongside with me server side admin appl, and i do not have cors issue. Maybe it some how related to the host itself? All my apps hosted on aws

I have used Odan’s approach. It works well for me (like most solutions provided by Odan :wink: )
So in my case everything looks good and is functional and I want to thank all those who helped me.

Cheers from Switzerland!

Hello, I have this problem. I need help.

Hi, i have the same issue with my vue app and I tried Odan’s approach but it did not work

Here is my code

<?php

namespace API\IpLocation;

use API\ClientIp\RemoteAddress;

use API\IpLocation\Core\Aggregator;

use API\Database\Classes\Middlewares\ApiKeyAuthMiddleware;
use API\Database\Classes\Middlewares\CorsMiddleware;
use API\Database\Classes\PreflightAction;
use API\Database\Utils\Constants;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

use Middlewares\TrailingSlash;
use Slim\Factory\AppFactory;

require_once '../vendor/autoload.php';

$app = AppFactory::create();

$app->setBasePath('/ip-location-api');

$app->add(new CorsMiddleware());
$app->addRoutingMiddleware();
$app->add(new TrailingSlash(false));
$app->add(new ApiKeyAuthMiddleware(Constants::API_KEYS));
$app->addErrorMiddleware(true, true, true);

$app->get('[/{ip}[/{source}]]', function (Request $request, Response $response) {
    try {
        $ip = $request->getAttribute('ip');
        $source = $request->getAttribute('source');

        if (!isset($ip) || empty($ip)) {
            $address = new RemoteAddress();

            $ip = $address->getIpAddress();
        }

        $source = (!isset($source) || empty($source)) ? 'auto' : $source;

        $aggregator = new Aggregator($ip, $source);

        $response->getBody()->write(json_encode($aggregator->fetchIpLocation()));

        return $response->withHeader('content-type', 'application/json')->withStatus(200);
    } catch (\Exception $e) {
        $error = array("message" => $e->getMessage());

        $response->getBody()->write(json_encode($error));

        return $response->withHeader('content-type', 'application/json')->withStatus(500);
    }
});

$app->options('[/{ip}[/{source}]]', new PreflightAction());

$app->run();

<?php

namespace API\Database\Classes\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;

final class CorsMiddleware implements MiddlewareInterface
{
    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 ?: '*');

        $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');

        return $response;
    }
}

<?php

namespace API\Database\Classes;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class PreflightAction
{
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        return $response;
    }
}