5

I am building an API in Yii2 and have added CORS and authentication. This works fine for all Create/Read/Update/Delete actions but not for custom actions. Has anyone experienced this before?

URL manager:

['class' => 'yii\rest\UrlRule', 'controller' => 'api/v1/user', 'pluralize' => false],

Controller behaviors:

public function behaviors()
{
    return ArrayHelper::merge([
            'corsFilter' => [
                'class' => Cors::className(),
            ],
            [
                'class' => HttpBearerAuth::className(),
                'except' => ['options',
                             'login',
                ],
            ],
        ], parent::behaviors()
    );
}

As mentioned, actions for CRUD are fine but a custom action such as http://domain.com/user/test will respond with a 401 Unauthorised response.

Is it not possible to get CORS and auth to work together on custom actions?

Edit: I should add that the issue (401) occurs only when a browser makes the OPTIONS request. Normal requests (curl,Postman) are not affected. The issue seems to occur with the RESTful,Cors,Auth combination.

Dubby
  • 2,290
  • 4
  • 27
  • 38

2 Answers2

11

try this:

public function behaviors()
{
    $behaviors = parent::behaviors();

    unset($behaviors['authenticator']);

    $behaviors['corsFilter'] = [
        'class' => Cors::className(),
        'cors' => [
            'Origin' => ['*'],
            'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
            'Access-Control-Request-Headers' => ['*'],
            'Access-Control-Allow-Credentials' => true,
        ],
    ];

    $behaviors['authenticator'] = [
        'class' =>  HttpBearerAuth::className(),
        'except' => ['options','login'],
    ];

    return $behaviors;
}

It will unset the default authenticator implemented by the parent controller to be sure that cors is treated first. Then we force cors to allow credentials before implementing your own authenticator.


The other thing that may raise that Unauthorized error is a not-found or wrong Options response as a browser request it first to get a list of allowed verbs. You may check that list in its headers response within your browser's network tab.

The general rule is when you ask your browser to perform a sensible verb like PUT, DELETE or POST to any url it may first send an OPTIONS request to that same url (check this) to check if that verb is allowed before sending the real request. So Yii should be configured to respond to all those OPTIONS verbs by performing the correct redirections.

The default CRUD actions implemented by ActiveController are using those default patterns:

'PUT,PATCH {id}' => 'update',
'DELETE {id}' => 'delete',
'GET,HEAD {id}' => 'view',
'POST' => 'create',
'GET,HEAD' => 'index',
'{id}' => 'options',
'' => 'options',

So whatever configurations you did implement in urlManager['rules'] be sure to not override the last 2 of them and if you are using custom patterns always remember to include its equivalent options verbs like in this example:

[
    'class' => 'yii\rest\UrlRule', 
    'controller' => ['account' => 'auth/account'], 
    'patterns' => [
        'POST,HEAD login'  => 'login',
        'POST,HEAD signup' => 'signup',
        'POST req-reset-pass' => 'request-password-reset',
        'POST reset-pass' => 'reset-password',
        // OPTTIONS VERBS
        'OPTIONS login' => 'options',
        'OPTIONS signup' => 'options',
        'OPTIONS req-reset-pass' => 'options',
        'OPTIONS reset-pass' => 'options',
    ]
],

The same applies when adding custom patterns within extraPatterns.


The Options action is implemented by default in ActiveController. it's code can be seen here. In case you are extending a different controller than ActiveController like maybe \yii\rest\Controller be sure to manually include it:

public function actions() 
{
    $actions = parent::actions();
    $actions['options'] = [
        'class' => 'yii\rest\OptionsAction',
        // optional:
        'collectionOptions' => ['GET', 'POST', 'HEAD', 'OPTIONS'],
        'resourceOptions' => ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
    ];
    return $actions;
}
Community
  • 1
  • 1
Salem Ouerdani
  • 7,596
  • 3
  • 40
  • 52
  • 1
    FIXED. Thanks so much for the thorough response. This had caused me so much pain. It was adding the patterns declaration to the URL manager that fixed it. I am forever grateful. – Dubby Apr 01 '16 at 00:52
  • I have just discovered that when I added the `patterns` attribute to the urlManager it has broken to `update` functionality (`PUT user/1` for example). Now when I attempt to update I get a `404 response`. Removing the patterns attribute corrects this. I can't seem to find a solution. Any ideas? – Dubby Apr 11 '16 at 06:17
  • 1
    I got around the issue mentioned directly above by adding another pattern `'PUT,PATCH {id}' => 'update',`. – Dubby Apr 11 '16 at 06:54
1

Just move your corsFilter block above the authenticator block like this:

public function behaviors()
    {
        return [
            'corsFilter' => [
                'class' => \yii\filters\Cors::className(),
            ],
            'authenticator' => [
                'class' => HttpBearerAuth::className(),
            ],
            'contentNegotiator' => [
                'class' => ContentNegotiator::className(),
                'formats' => [
                    'application/json' => Response::FORMAT_JSON,
                ],
            ],

        ];
    }
Roberto Caboni
  • 7,252
  • 10
  • 25
  • 39
Roman
  • 11
  • 1