I have a yii2 app for the backend and I am using react for the frontend. I try now through an api call the login function. But I get this error when I try to login on the frontend:
login/:1 Access to XMLHttpRequest at 'http://localhost:8080/v1/user/login' from origin 'http://localhost:3000' 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.
What I already tried. I found this link:
https://www.yiiframework.com/doc/api/2.0/yii-filters-cors
But didn't resolved the problem.
So I try to configure the controller from yii2 , that it can communicate with the frontend.
Controller:
<?php
namespace app\modules\v1\controllers;
use Yii;
use app\models\User;
use yii\rest\ActiveController;
use yii\web\Response;
use yii\filters\Cors;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
protected $_verbs = ['POST','OPTIONS'];
public function behaviors(){
$behaviors = parent::behaviors();
// remove auth filter before cors if you are using it
unset($behaviors['authenticator']);
// add CORS filter
$behaviors['corsFilter'] = [
'class' => \yii\filters\Cors::class,
'cors' => [
'Origin' => ['*'],
'Access-Control-Allow-Origin' => ['*', 'http://localhost:3000'],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400,
'Access-Control-Expose-Headers' => ['X-Pagination-Current-Page'],
],
];
$behaviors['authenticator']['except'] = ['options', 'login'];
return $behaviors;
}
public function actionLogin()
{
$username = Yii::$app->request->post('username');
$password = Yii::$app->request->post('password');
$user = User::findByUsername($username);
if ($user && $user->validatePassword($password)) {
return ['status' => 'success', 'message' => 'Login successful'];
} else {
Yii::$app->response->statusCode = 401;
return ['status' => 'error', 'message' => 'Invalid username or password'];
}
}
public $username;
public $password;
public $rememberMe = true;
private $_user;
public function rules()
{
return [
[['username', 'password'], 'required'],
['rememberMe', 'boolean'],
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* @param string $attribute the attribute currently being validated
* @param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect username or password.');
}
}
}
protected function getUser()
{
if ($this->_user === null) {
$this->_user = User::findByUsername($this->username);
}
return $this->_user;
}
and user:
<?php
// phpcs:ignoreFile
namespace app\models;
use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
use yii\filters\Cors;
/**
* This is the model class for table "user".
*
*/
class User extends \yii\db\ActiveRecord implements IdentityInterface
{
const STATUS_DELETED = 0;
const STATUS_INACTIVE = 9;
const STATUS_ACTIVE = 10;
public function behaviors()
{
return [
TimestampBehavior::class,
];
}
public static function tableName()
{
return '{{%user}}';
}
public function rules()
{
return [
[['status', 'created_at', 'updated_at'], 'default', 'value' => null],
[['status', 'created_at', 'updated_at'], 'integer'],
[['username', 'password_hash', 'password_reset_token', 'email', 'verification_token'], 'string', 'max' => 255],
[['auth_key'], 'string', 'max' => 32],
[['email'], 'unique'],
[['password_reset_token'], 'unique'],
[['username'], 'unique'],
['status', 'default', 'value' => self::STATUS_ACTIVE],
['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_INACTIVE, self::STATUS_DELETED]],
];
}
public static function findIdentity($id)
{
return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]);
}
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['auth_key' => $token]);
}
public function getId()
{
return $this->getPrimaryKey();
}
public function getAuthKey()
{
return $this->auth_key;
}
}
and web.php file:
<?php
// phpcs:ignoreFile
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
$config = [
'id' => 'basic',
'name' => 'internetsuite 2.0',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'modules' => [
'v1' => [
'class' => 'app\modules\v1\Module',
],
],
'components' => [
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => 'OEtCunrAfQNETtmUSDnZw1JPHTB44i3A',
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
],
'errorHandler' => [
'errorAction' => 'site/error',
],
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'viewPath' => '@app/mail',
// send all mails to a file by default.
'useFileTransport' => true,
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => $db,
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'class' => 'yii\rest\UrlRule',
'controller' => 'v1/user',
'patterns'=>[
'POST' => 'signup',
'POST login' => 'login',
'OPTIONS login' => 'options',
],
],
],
],
'params' => $params,
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
}
return $config;
and react login call looks:
/* eslint-disable newline-before-return */
import { UserProfileToken } from "../models/User";
import axios from "axios";
import { handleError } from "../helpers/ErrorHandler";
const api = "http://localhost:8080/v1/";
export const loginAPI = async (username: string, password: string) => {
try {
const data = await axios.post<UserProfileToken>(api + "user/login", {
username: username,
password: password,
});
return data;
} catch (error) {
handleError(error);
}
};
}
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'class' => 'yii\rest\UrlRule',
'controller' => 'v1/user',
'patterns' => [
'POST' => 'signup',
'POST login' => 'login',
'OPTIONS login' => 'options',
],
],
],
I get this error:
Unknown Property – yii\base\UnknownPropertyException
Setting unknown property: yii\web\UrlRule::POST
There are some issues with your rules settings for
UrlManager. Request being blocked by CORS policies despite usingyii\filters\Corsis the result of these issues too.First of all, in this part of configuration:
The part
'POST' => 'v1/user/signup'doesn't work. I'm not sure if you just left out too much when copying your code to question or if this is how you have your settings in your project. The issue is that inyii\rest\UrlRuleconfiguration for matching methods to actions have to be part of$patternsor$extraPatternsproperties. The main difference between these two properties is that$patternscontain some default patterns used by actions inyii\rest\ActiveConroller. So the rest rule configuration should look for example like this:The reason why the signup might seem to work was probably because the request matched default post pattern. It looks like this
'POST' => 'create'. The creation of user was handled byyii\rest\CreateActionwhich is part of defaultyii\rest\ActiveControlleractions.Another issue is that if you want your rest controller to handle url in format like
module/controller/action(eg.v1/user/login) you have explicitly add the pattern for this route. This is becauseyii\rest\UrlRuledoesn't use the usualcontroller/actionmatching standard rules does. So, with added pattern forv1/user/loginPOST requests the config might look like this:Now we are finally getting to CORS blocking part. When browser is going to send AJAX POST request the first thing it does is something called "CORS preflight". This means it tries to send OPTIONS request to same url it's going to send the POST request to retrieve CORS headers first. Your
UrlManagerdoesn't know how to handle OPTIONS request forv1/user/loginroute. The request doesn't reach yourUserControllerandyii\filters\Corsis never applied to that request. Because of that browser doesn't get proper CORS headers and the POST request is blocked. To avoid that we need to add pattern for OPTIONS request to our configuration, for example like this:To avoid CORS blocking using
optionsaction that already exists amongyii\rest\ActiveControllerdefault actions is good enough. For proper REST API you might want to make sure that only supported methods are returned in response to OPTIONS request.