I extend ActiveController and set CORS filter in my application :
'corsFilter' => [
'class' => \yii\filters\Cors::className(),
'cors' => [
'Origin' => ["*"],
'Access-Control-Allow-Headers' => ["Application-Key"]
]
],
I also added to the authenticator:
'except' => ["options"]
I created test client application:
$.ajax({
method: 'GET',
url: "https://my-app-url.com/entity/1",
headers: {
"Application-Key":"123456someappkey"
}
}).done(function(data) {
console.log(data);
});
I expected to receive data from application
I got next error:
Access to XMLHttpRequest at 'https://my-app-url.com/entity/1' from origin 'https://fiddle.jshell.net' has been blocked by CORS policy: Request header field Application-Key is not allowed by Access-Control-Allow-Headers in preflight response.
| Q | A
| ---------------- | ---
| Yii version | 2.0.15.1
| PHP version | 7.2
| Operating system | Debian GNU/Linux 9
Is there any progress about this? I have the very same problem and noticed the only way I can extend the allowed request headers is by creating a custom filter like this
class Cors extends \yii\filters\Cors
{
/**
* Extract CORS headers from the request.
* @return array CORS headers to handle
*/
public function extractHeaders()
{
$headers = [];
$_SERVER['HTTP_ACCESS_CONTROL_ALLOW_HEADERS'] = 'MY_CUSTOM_HEADER, X-CSRF-Token, Origin, X-Requested-With, Content-Type, Accept';
$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] = 'MY_CUSTOM_HEADER, X-CSRF-Token, Origin, X-Requested-With, Content-Type, Accept';
foreach (array_keys($this->cors) as $headerField) {
$serverField = $this->headerizeToPhp($headerField);
$headerData = $_SERVER[$serverField] ?? null;
if ($headerData !== null) {
$headers[$headerField] = $headerData;
}
}
return $headers;
}
}
The extractHeaders defines the allowed headers, if your custom key is not within those you don't really have a chance to include your own. Is that intended? So the property Access-Control-Allow-Headers only allows you to limit the allowed headers you are not allowed to extend them
Same issue with ES6 fetch request.
@blacksmoke26 any easy way to reproduce it?
I have fixed it already, please let me share the code:
../config/main.php file
use api\components\handlers\{
RequestHandler,
ResponseHandler,
};
use yii\web\Response;
$config = [
//....
'components' => [
'response' => [
'class' => Response::class,
'format' => Response::FORMAT_JSON,
'on beforeSend' => function ($event) {
(new ResponseHandler)->beforeSend ($event->sender);
},
'on afterSend' => function ($event) {
(new ResponseHandler)->afterSend($event->sender);
},
],
],
'on beforeRequest' => function () {
(new RequestHandler)->beforeSend();
},
'on afterRequest' => function () {
(new RequestHandler)->afterSend();
},
//....
];
api\components\handlers\RequestHandler.php file
namespace api\components\handlers;
use Yii;
/**
* Class RequestHandler
* @package api\components\response
*/
class RequestHandler {
/**
* Execute event before response send
*/
public function beforeSend () : void {
/** @var string $pathInfo */
$pathInfo = Yii::$app->request->pathInfo;
// fixed: UrlRule Problem with trailing slash (/)
if ( !empty($pathInfo) && '/' === \substr ($pathInfo, -1) ) {
Yii::$app->response->redirect('/' . \rtrim($pathInfo, '/'))->send();
exit;
}
// No output for OPTIONS
if ( 'OPTIONS' === Yii::$app->getRequest ()->getMethod () ) {
Yii::$app->end ();
exit;
}
}
/**
* Execute event after request sent
*/
public function afterSend () : void {}
}
api\components\handlers\ResponseHandler.php file
namespace api\components\handlers;
use common\utils\Configuration;
use Yii;
use yii\web\Response;
/**
* Class ResponseHandler
* @package api\components\response
*/
class ResponseHandler {
/**
* Execute event before response send
* @param \yii\web\Response $response Response object
*/
public function beforeSend ( Response $response ) : void {
// CORS headers
$this->setupCorsHeaders ($response);
// Format output response
$this->responseFormatter ($response);
}
/**
* Execute event after response sent
* @param \yii\web\Response $response Response object
*/
public function afterSend ( Response $response ) : void {}
/**
* Format output response
* @param \yii\web\Response $response Response object
*/
private function responseFormatter ( Response $response ) : void {
/** @var array $data */
$data = $response->data;
if ( !$response->isSuccessful ) {
if ( !$response->statusCode ) {
$response->statusCode = 400;
}
if ( \is_countable ($data) && \count ($data) ) {
$response->data = [];
unset($data['previous']);
$response->data = $data;
}
return;
}
if ( \null !== $data ) {
if ( \is_countable($data) && \count ($data) ) {
$response->data = [];
$response->data['data'] = $data;
}
}
if ( !$response->statusCode ) {
$response->statusCode = 200;
}
}
/**
* Setup CORS headers
* @param \yii\web\Response $response Response object
*/
private function setupCorsHeaders ( Response $response ) : void {
/** @var \yii\web\HeaderCollection $headers */
$headers = $response->headers;
/** @var array $allowedHeaders */
$allowedHeaders = Configuration::current ('security.cors.allowed.headers', ['toArray'=>\true]);
$headers->set ('Access-Control-Allow-Origin', static::getOrigin ());
$headers->set ('Access-Control-Allow-Credentials', 'true');
$headers->set ('Access-Control-Max-Age', (int) Configuration::current ('security.cors.maxAge'));
$headers->set ('Access-Control-Allow-Headers', \implode (', ', $allowedHeaders));
$headers->set ('Access-Control-Allow-Methods', 'GET, OPTIONS, POST, DELETE, PUT, HEAD, PATCH');
}
/**
* Get origin from request
* @return string
*/
private static function getOrigin () : string {
/** @var \yii\web\HeaderCollection $headers */
$headers = Yii::$app->request->headers;
/** @var string[] $whitelistOrigins */
$whitelistOrigins = Configuration::current ('security.cors.allowed.origins', ['toArray'=>\true]);
/** @var string $origin */
$origin = $headers->get ('origin')
?? $headers->get ('host')
?? \null;
foreach ( $whitelistOrigins as $host ) {
if ( \false !== \strpos ($origin, $host) ) {
return $origin;
}
}
return Configuration::current ('uri.baseUrl');
}
}
At end, main controller file /components/Controller.php
namespace api\components;
use common\utils\Configuration;
use Yii;
/**
* Class Controller
* @package api\components
*/
class Controller extends \yii\web\Controller {
/**
* @inheritdoc
*/
public function init() {
parent::init();
// Disabled CSRF validation
$this->enableCsrfValidation = \false;
// Disabled layouts
$this->layout = \false;
}
/**
* @inheritDoc
*/
public function behaviors () {
return [
'hostControl' => [
'class' => \yii\filters\HostControl::class,
'allowedHosts' => [
'*.' . Configuration::current ('uri.baseDomain'),
],
],
'authenticator' => [
'class' => \common\components\jwt\filters\AutoAuth::class,
'except' => ['OPTIONS'],
],
'access' => [
'class' => \yii\filters\AccessControl::class,
'except' => ['OPTIONS'],
],
'verbs' => [
'class' => \yii\filters\VerbFilter::class,
'actions' => [
'*' => ['OPTIONS'],
],
],
];
}
}
The method Configuration::current() uses following JSON to return values:
{
"uri": {
"baseDomain": "domain.test"
},
"security": {
"cors": {
"allowed": {
"origins": ["domain.test"],
"headers": [
"Accept", "Origin", "X-Auth-Token",
"Content-Type", "Authorization", "X-Requested-With",
"Accept-Language", "Last-Event-ID", "Accept-Language",
"Cookie", "Content-Length", "WWW-Authenticate", "X-XSRF-TOKEN",
"withcredentials", "x-forwarded-for", "x-real-ip",
"x-customheader", "user-agent", "keep-alive", "host",
"connection", "upgrade", "dnt", "if-modified-since", "cache-control"
]
},
"maxAge": 86400
}
}
}
Above is my code to handle CORS, ES6 fetch and jQuery $.ajax both are comfortable with that.
@blacksmoke26, you just set necessary headers manually. I did roughly the same thing in my case but I'm not sure it is a good solution. It should be fixed in the framework core or at least clarified in documentation.
@Qclanton, nope, it's not! but was in hurry! So did that on purpose.
My solution was simply adding the following line at the top of the index.php, and it works though it seems anti-pattern:
header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization');
I solved adding this line before of return $responseHeaders; on function prepareHeaders($requestHeaders) in vendor/yiisoft/yii2/filters/Cors.php:
$responseHeaders['Access-Control-Allow-Headers'] = $this->cors['Access-Control-Allow-Headers'];
But I did not like to edit this in a vendor dependency, so...
As I'm using advanced Yii2 application, I created the class file common/filters/Cors.php like this:
<?php
namespace common\filters;
class Cors extends \yii\filters\Cors {
public function prepareHeaders($requestHeaders) {
$responseHeaders = parent::prepareHeaders($requestHeaders);
if (isset($this->cors['Access-Control-Allow-Headers'])) {
$responseHeaders['Access-Control-Allow-Headers'] = implode(', ', $this->cors['Access-Control-Allow-Headers']);
}
return $responseHeaders;
}
}
In my controller, I did put this:
public static function allowedDomains() {
return [$_SERVER["REMOTE_ADDR"], 'http://localhost:4200'];
}
function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['contentNegotiator'] = [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
];
return array_merge($behaviors, [
'corsFilter' => [
'class' => \common\filters\Cors::className(),
'cors' => [
// restrict access to domains:
'Origin' => static::allowedDomains(),
'Access-Control-Request-Method' => ['POST', 'GET', 'OPTIONS'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 3600, // Cache (seconds)
'Access-Control-Allow-Headers' => ['authorization','X-Requested-With','content-type', 'some_custom_header']
],
],
]);
return $behaviors;
}
Most helpful comment
Same issue with ES6 fetch request.