Yii2 REST+ Angular Cross Domain CORS - angularjs

I have developed Angular & Yii2 REST service. Have problem in cross domain.
Here below add my angular & Yii2 REST Code.
AngularJs : (like 'http://organization1.example.com','http://organization2.example.com',....)
$http.defaults.useXDomain = true;
$http.defaults.withCredentials = true;
$http.defaults.headers.common['Authorization'] = 'Bearer ' + MYTOKEN
My Request from Angular Controller:
apiURL = 'http://api.example.com';
$http.get(apiURL + '/roles')
.success(function (roles) { })
.error(function () { });
Yii2 .htaccess: (REST URL like 'http://api.example.com')
Header always set Access-Control-Allow-Origin: "*"
Header always set Access-Control-Allow-Credentials: true
Header always set Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization,X-Requested-With, content-type"
Yii2 My Behaviour:
public function behaviors() {
$behaviors = parent::behaviors();
$behaviors['corsFilter'] = [
'class' => Cors::className(),
'cors' => [
'Origin' => ['*'],
'Access-Control-Expose-Headers' => [
'X-Pagination-Per-Page',
'X-Pagination-Total-Count',
'X-Pagination-Current-Page',
'X-Pagination-Page-Count',
],
],
];
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::className(),
'except' => ['options'],
];
$behaviors['contentNegotiator'] = [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
];
return $behaviors;
}
Problem
From my angular request is 'GET' method, but it will goes 'OPTIONS' method & return 401 Unauthorized error(CORS). because the request Authorization header is not send.

Update:
As pointed by #jlapoutre, this is now well described in official docs:
Adding the Cross-Origin Resource Sharing filter to a controller is a
bit more complicated than adding other filters described above,
because the CORS filter has to be applied before authentication
methods and thus needs a slightly different approach compared to other
filters. Also authentication has to be disabled for the CORS Preflight
requests so that a browser can safely determine whether a request can
be made beforehand without the need for sending authentication
credentials. The following shows the code that is needed to add the
yii\filters\Cors filter to an existing controller that extends from
yii\rest\ActiveController:
use yii\filters\auth\HttpBasicAuth;
public function behaviors()
{
$behaviors = parent::behaviors();
// remove authentication filter
$auth = $behaviors['authenticator'];
unset($behaviors['authenticator']);
// add CORS filter
$behaviors['corsFilter'] = [
'class' => \yii\filters\Cors::className(),
];
// re-add authentication filter
$behaviors['authenticator'] = $auth;
// avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
$behaviors['authenticator']['except'] = ['options'];
return $behaviors;
}
Old Answer (deprecated)
There is an ordering issue when merging with parent::behaviors(). Full details here.
I would recommend not defining keys when merging with parent array:
public function behaviors()
{
return \yii\helpers\ArrayHelper::merge([
[
'class' => \yii\filters\Cors::className(),
'cors' => [...],
],
[
'class' => \yii\filters\auth\HttpBearerAuth::className(),
'except' => ['options'],
],
[
'class' => ContentNegotiator::className(),
'formats' => [...],
]
], parent::behaviors());
}

In your controller:
use yii\filters\Cors;
...
public function behaviors()
{
return array_merge([
'cors' => [
'class' => Cors::className(),
#special rules for particular action
'actions' => [
'your-action-name' => [
#web-servers which you alllow cross-domain access
'Origin' => ['*'],
'Access-Control-Request-Method' => ['POST'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => null,
'Access-Control-Max-Age' => 86400,
'Access-Control-Expose-Headers' => [],
]
],
#common rules
'cors' => [
'Origin' => [],
'Access-Control-Request-Method' => [],
'Access-Control-Request-Headers' => [],
'Access-Control-Allow-Credentials' => null,
'Access-Control-Max-Age' => 0,
'Access-Control-Expose-Headers' => [],
]
],
], parent::behaviors());
}
Documentation

issue with your code you are not unsetting auth at very start
public function behaviors() {
$behaviors = parent::behaviors();
/*unset here*/
unset($behaviors['authenticator']);
$behaviors['corsFilter'] = [
'class' => Cors::className(),
'cors' => [
'Origin' => ['*'],
'Access-Control-Expose-Headers' => [
'X-Pagination-Per-Page',
'X-Pagination-Total-Count',
'X-Pagination-Current-Page',
'X-Pagination-Page-Count',
],
],
];
/*re-set here*/
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::className(),
'except' => ['options'],
];
$behaviors['contentNegotiator'] = [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
];
return $behaviors;
}

Related

The request object does not contain the required `authentication` attribute when trying to API login

I'm trying to skip CSRF token required middleware for API prefix , Here I'm using cakephp authentication plugin 2.x. I have tried below code to avoid token for api prefix
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error')))
$csrf = new CsrfProtectionMiddleware();
$csrf->skipCheckCallback(function ($request) {
// Skip token check for API URLs.
if ($request->getParam('prefix') === 'Api') {
return true;
}
});
// add Authentication after RoutingMiddleware
$middlewareQueue->add($csrf);
}
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$authenticationService = new AuthenticationService([
'unauthenticatedRedirect' => Router::url('/login'),
'queryParam' => 'redirect',
]);
// Load identifiers, ensure we check email and password fields
$identifierSettings = [
'fields' => [
'username' => 'email',
'password' => 'password',
],
];
$authenticationService->loadIdentifier('Authentication.Password', $identifierSettings);
$authenticationService->loadAuthenticator('Authentication.Session');
// Configure form data check to pick email and password
$authenticationService->loadAuthenticator('Authentication.Form', [
'fields' => [
'username' => 'email',
'password' => 'password',
],
// 'loginUrl' => Router::url('/admin'),
]);
return $authenticationService;
}
I have written below controller login action for login
public function login()
{
$result = $this->Authentication->getResult();
if($result->isValid())
{
$user = $result->getData();
$this->set('user',$user);
$this->viewBuilder()->setOption('serialize', 'user');
}
}
Agter send request using postman , I have got below response
I have nothing set in authentication option in postman, Do I need anything to set in postman ? What is this authentication attribute ?

Make Laravel request from array with objects

I have an array that makes in front end with js and pass that to my controller with ajax.
Ajax:
var values = [{FirstName: "fff"},{LastName: null}]
$.ajax({
method: "POST",
url: "/api/store-step",
data: { values: values, step: activePanelNum }
}).fail(function (jqXHR, textStatus, error,result) {
console.log(jqXHR.responseJSON.errors);
}).done(function( result ) {
console.log(result);
});
structure of array is this:
[{FirstName: "fff"},{LastName: null}]
Controller:
public function storeSteps(Request $request)
{
$validator = Validator::make($request->values, [
'FirstName' => 'required',
'LastName' => 'required',
]);
if ($validator->fails()) {
return response()->json(['success'=>false, 'errors' => $validator->getMessageBag()->toArray()],422);
}
}
I can't validate this array with request validation Laravel. Now I'm going to turn this array into a Larval request to apply the rules to it.
Can any one helps?
you can validate array element like this
$validator = Validator::make($request->all(), [
'values' => 'required',
'values.*.FirstName' => 'required',
'values.*.lastName' => 'required','
]);
by using . you can access an index in a array and * apples all indexes in the array.

CakePHP 3 JWT-Auth gives 401 Unauthorized error

I'm using CakePHP 3.6 and JWT Auth to enable token-based authentication in my application and frontend is written in Angular 6.
My login controller is like
<?php
namespace App\Controller\Api;
use Cake\Event\Event;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Utility\Security;
use Crud\Controller\Component\CrudComponent;
use Firebase\JWT\JWT;
class UsersController extends AppController
{
public function initialize()
{
parent::initialize();
$this->Auth->allow(['add', 'token']);
}
public function token()
{
$user = $this->Auth->identify();
if (!$user) {
throw new UnauthorizedException('Invalid username or password');
}
$this->set([
'success' => true,
'data' => [
'token_type' => 'Bearer',
'expires_in' => 604800,
'token' => JWT::encode([
'sub' => $user['id'],
// 'exp' => time() + 604800
],
Security::getSalt())
],
'_serialize' => ['success', 'data']
]);
}
}
AppController.php contents
namespace App\Controller\Api;
<?php
use Cake\Controller\Controller;
class AppController extends Controller
{
use \Crud\Controller\ControllerTrait;
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Crud.Crud', [
'actions' => [
'Crud.Index',
'Crud.View',
'Crud.Add',
'Crud.Edit',
'Crud.Delete'
],
'listeners' => [
'Crud.Api',
'Crud.ApiPagination'
]
]);
$this->loadComponent('Auth', [
'storage' => 'Memory',
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password'
],
'finder' => 'auth'
],
'ADmad/JwtAuth.Jwt' => [
'parameter' => 'token',
'userModel' => 'Users',
'finder' => 'auth',
'fields' => [
'username' => 'id'
],
'queryDatasource' => true
]
],
'unauthorizedRedirect' => false,
'checkAuthIn' => 'Controller.initialize'
]);
}
}
On sending request from the angular application to generate token works fine and following response is received.
But when using the token to send the request to other endpoints giving an error
401: Unauthorized access
The request/response header has token
What I tried?
I tried with disabling exp while generating an access token.
tried with disabling debug in CakePHP application.
It is working great when CakePHP server application is run locally.
in your .htaccess try this rule (if mod_rewrite is activated) :
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
With the Bitnami stack of LAMP (on EC2 AWS instance for example), the php-fdm module filter the header of every requests, and the "authorization" header is screwed up.
With this line, you can force to create a $_HTTP variable with the original Authorization header.
Regards
Check in cakephp code if you are receiving the AUTHORIZATION in headers.

Laravel validation for arrays

I have this request:
GET http://example.com/test?q[]=1&q[]=2&q[]=3
And I have this route:
Route::get('test', function(Request $req) {
$req->validate(['q' => 'array']);
});
How should I do to add other validation rules to each element of this array using Laravel validator? For example, I want to check that each q value has a minimum of 2.
Thank you for your help.
Take a look at the documentation about validating arrays.
$validator = Validator::make($request->all(), [
'person.*.email' => 'email|unique:users',
'person.*.first_name' => 'required_with:person.*.last_name',
]);
You can also do this in your controller using the Request object, documentation about validation logic.
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
// The blog post is valid...
}
There is a third option for when you have a lot of validation rules and want to separate the logic in your application. Take a look at Form Requests
1) Create a Form Request Class
php artisan make:request StoreBlogPost
2) Add Rules to the Class, created at the app/Http/Requestsdirectory.
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
3) Retrieve the request in your controller, it's already validated.
public function store(StoreBlogPost $request)
{
// The incoming request is valid...
// Retrieve the validated input data...
$validated = $request->validated();
}
You can do:
Route::get('test', function(Request $req) {
$req->validate([
'q' => 'array',
'q.*' => 'min:2'
]);
});
For more information on validation of arrays, see => laravel.com/docs/5.6/validation#validating-arrays
Suppose I got an array of users
users: [
{
"id": 1,
"name": "Jack",
},
{
"id": 2,
"name": "Jon"
}
]
I would validate it like below :
$request->validate([
'users[*]'=> [
"id" => ["integer", "required"],
"name" => ["string", "required"]
]
]);
Here * acts as a placeholder

CakePHP 3: Not Sure Why Cache::read() Is Not Working

I have a SettingsSiteTable object with the following method for reading settings in the database and storing the results in a cache.
// GET ALL CONFIG SETTINGS
function getConfigs(){
if(($settings_site = Cache::read($this->key)) === false) {
$settings_site = $this->find('list', [
'keyField' => 'key',
'valueField' => 'value'
])->toArray();
Cache::write($this->key, $settings_site, 'settings');
}
return $settings_site;
} // END GET CONFIGS FUNCTION
$this->key is 'SettingsSite' and there appears to be no issue with that. I have also added use Cake\Cache\Cache to the table object file.
My cache config in the app.php file is as follows:
'Cache' => [
'default' => [
'className' => 'File',
'path' => CACHE,
],
'settings' => [
'className' => 'File',
'duration' => '+6 hours',
'path' => CACHE . 'settings/',
],
'_cake_core_' => [
// ...
],
'_cake_model_' => [
// ...
],
],
The cache appears to save successfully. I am able to view the cache file located at tmp/cache/settings/settings_site (not sure what the file extension is)
However, if I change the data in the database and refresh the page, the updated information is displayed meaning it is not reading from the cache correctly and it is re-querying the results.
What am I doing wrong? I got this code directly from the 3.X Cookbook: Writing to a Cache
Silly me I realized I left out the config option during Cache:read() so it was reading from the default config but I was writing to the 'settings' config.
// GET ALL CONFIG SETTINGS
function getConfigs(){
if(($settings_site = Cache::read($this->key, 'settings')) === false) {
$settings_site = $this->find('list', [
'keyField' => 'key',
'valueField' => 'value'
])->toArray();
Cache::write($this->key, $settings_site, 'settings');
}
return $settings_site;
} // END GET CONFIGS FUNCTION

Resources