Access-Control-AllowOrigin : multiple domains


#1

Hello folks,

Is it possible to have multiple domains allowed with CORS?

E,g, could middleware be set up like…

return $response
        ->withHeader('Access-Control-Allow-Origin', 'https://somedomain.com')
        ->withHeader('Access-Control-Allow-Origin', 'http://otherdomain.com')

In my case, my staging server is at different domain than live site and I’d like to support both.

Thanks!


#2

I know almost nothing about CORS, but looking at the PSR-7 docs you might try this:

return $response
    ->withHeader('Access-Control-Allow-Origin', 'https://somedomain.com')
    ->withAddedHeader('Access-Control-Allow-Origin', 'http://otherdomain.com')

#3

Thanks for the tip tflight! That didn’t work but I’ll check out those docs some more.


#4

According to the documentation at Mozilla, you can have only one Access-Control-Allow-Origin header:

A returned resource may have one Access-Control-Allow-Origin header, with the following syntax:

Access-Control-Allow-Origin: <origin> | *

You can use * for any origin:

return $response
        ->withHeader('Access-Control-Allow-Origin', '*');

#5

Unfortunately you cannot specify multiple domains. You either need to create an .htaccess/apache rule: credit which may or may not work for you if you don’t have access to apache modules.

<FilesMatch "\.(ttf|otf|eot|woff|js|css|woff2)$">
    <IfModule mod_headers.c>
        SetEnvIf Origin "^http(s)?:\/\/(www\.|dev\.|local\.)?(domain\.com|domain2\.com)$" AccessControlAllowOrigin=$0
        Header add Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
    </IfModule>
</FilesMatch>

or have a wildcard which doesn’t make sense to me. It seems to either be an all or very restrictive approach, which then most dev’s will opt for the very easy wildcard ‘*’ approach.

I wrote a Slim middleware, (which can be installed via composer) since I needed the ability to allow my API to be accessed by multiple domains and restrict what methods I allow. I also prefer having this ability in php instead of an .htaccess file. This is a slightly modified version of what I’m using since I’m using Slim’s DI Container. I moved all the settings into the same file. In other words make sure to TEST TEST TEST it as I’m not using this exact implementation.

CorsMiddleware.php

<?php
namespace Middleware;

class CorsMiddleware
{
    /**
     * Associative array with domain => [allowed methods] list
     * @var array
     */
    protected $cors = [
        'https://somedomain.com' => ['GET', 'POST'],
        'http://somedomain.com' => ['GET', 'POST'],
        'https://dev.somedomain.com' => ['DELETE', 'PUT', 'POST']
    ];

    /**
     * Middleware invokable class
     *
     * @param  \Psr\Http\Message\ServerRequestInterface $request  PSR7 request
     * @param  \Psr\Http\Message\ResponseInterface      $response PSR7 response
     * @param  callable                                 $next     Next middleware
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function run($request, $response, $next)
    {
        $response = $next($request, $response);
        $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : 'none';
        return $this->getResponse($response, $origin, $this->cors);
    }

    /**
     * Gets allow method string of comma separated http verbs
     * @param  string   $origin origin domain
     * @param  array    $cors   access list with methods
     * @return string           comma delimited string of methods
     */
    private function getAllowedMethodsString($cors, $origin)
    {
        $methods = $cors[$origin];
        if (is_array($methods)) {
            $methods = implode(', ', $methods);
        }
        return $methods;
    }

    /**
     * Gets the proper origin header value
     * @param  array    $cors   cors config
     * @param  string   $origin http_origin
     * @return string           origin value
     */
    private function getOriginHeader($cors, $origin)
    {
        if (isset($cors['*'])) {
            return '*';
        }
        return $origin;
    }

    /**
     * Gets appropriate response object
     * @param  \Psr\Http\Message\ResponseInterface $response PSR7 Response
     * @param  string                               $origin  origin domain
     * @param  array                                $cors    access list with methods
     * @return \Psr\Http\Message\ResponseInterface $response PSR7 Response
     */
    private function getResponse($response, $origin, $cors)
    {
        if (isset($cors['*'])) {
            $origin = '*';
        }

        if (!isset($cors[$origin])) {
            return $response;
        }

        return $response
        ->withHeader('Access-Control-Allow-Origin', $this->getOriginHeader($cors, $origin))
        ->withHeader('Access-Control-Allow-Methods', $this->getAllowedMethodsString($cors, $origin));
    }
}

index.php

.
.
$app->add('\Middleware\CorsMiddleware:run');
.
.

You may wonder why Allow-Methods may not appear to be working when you test ( you will test… right?!) , this got me as well, and I found the answer:

Simple cross-origin requests generated outside this specification (such as cross-origin form submissions using GET or POST or cross-origin GET requests resulting from script elements) typically include user credentials, so resources conforming to this specification must always be prepared to expect simple cross-origin requests with credentials.

Because of this, resources for which simple requests have significance other than retrieval must protect themselves from Cross-Site Request Forgery (CSRF) by requiring the inclusion of an unguessable token in the explicitly provided content of the request.

credit

Also mind your protocols! if you want http and https you will need to include both in the $cors list. Hope that helps.


#6

That’s fantastic. I’ll test that out once I get a chance to wrap my head around the code. Thanks!


#7

Hope it works for you, I threw the middleware up on github and made some modifications and added examples.