How to resolve the error react with yii2, has been blocked by CORS policy: Response?

49 Views Asked by At

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
2

There are 2 best solutions below

2
Michal Hynčica On BEST ANSWER

There are some issues with your rules settings for UrlManager. Request being blocked by CORS policies despite using yii\filters\Cors is the result of these issues too.

First of all, in this part of configuration:

[
    'class' => 'yii\rest\UrlRule',
    'controller' => 'v1/user',
    'POST' => 'v1/user/signup',
]

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 in yii\rest\UrlRule configuration for matching methods to actions have to be part of $patterns or $extraPatterns properties. The main difference between these two properties is that $patterns contain some default patterns used by actions in yii\rest\ActiveConroller. So the rest rule configuration should look for example like this:

[
    'class' => 'yii\rest\UrlRule',
    'controller' => 'v1/user',
    'patterns' => [
        'POST' => 'signup',
        // ... patterns for other HTTP methods
    ]
]

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 by yii\rest\CreateAction which is part of default yii\rest\ActiveController actions.

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 because yii\rest\UrlRule doesn't use the usual controller/action matching standard rules does. So, with added pattern for v1/user/login POST requests the config might look like this:

[
    'class' => 'yii\rest\UrlRule',
    'controller' => 'v1/user',
    'patterns' => [
        'POST' => 'signup',
        'POST login' => 'login',
        // ... patterns for other HTTP methods
    ]
]

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 UrlManager doesn't know how to handle OPTIONS request for v1/user/login route. The request doesn't reach your UserController and yii\filters\Cors is 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:

[
    'class' => 'yii\rest\UrlRule',
    'controller' => 'v1/user',
    'patterns' => [
        'POST' => 'signup',
        'POST login' => 'login',
        'OPTIONS login' => 'options',
        // ... patterns for other HTTP methods
    ]
]

To avoid CORS blocking using options action that already exists among yii\rest\ActiveController default 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.

0
mightycode Newton On

I still don't understand it. And I have to deep dive more in to this stuff. But it works. In the UserController I added this

public function behaviors()
{
      // avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
        $behaviors['authenticator']['except'] = [
            'options',
            'login',
            'signup',                  
        ];

}

And It doesn't throw anymore all the CORS errors stuff