I'm using the plugin Authentication 2 in cakephp 4.
I would like to throw an UnauthenticatedException when a user is not logged in and in the case of ajax request.
The goal is to catch the exception in JSON.
Here is my code from server :
// in src/Controller/Admin/AdminController.php
use Authentication\Authenticator\UnauthenticatedException;
class AdminController extends AppController {
public function initialize(): void
{
parent::initialize();
$this->loadComponent('Authentication.Authentication');
}
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
// The server receives an ajax request and the user is not logged in (any more), an UnauthenticatedException is thrown
if ($this->request->is('ajax') && $this->request->getAttribute('identity') === null) {
throw new UnauthenticatedException('Please log in');
}
}
}
Here is my code from client :
$.ajax({
dataType: 'json';
type: 'POST',
data: $(form).serialize(),
// [...]
})
// [...]
.fail(function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.responseJSON); // There's no responseJSON in jqXHR...
alert("(" + errorThrown + ")" + jqXHR.responseJSON.message);
if (errorThrown == 'Unauthenticated') {
location.reload();
}
});
The problem is that there's no responseJSON in jqXHR.
Why is any other Exception (e.g UnauthorizedException that I used before) generating responseJSON in the return and not UnauthenticatedException ?
How to do to make it work with UnauthenticatedException ?
The authentication middleware by default re-throws unauthenticated exceptions, that is unless you configure the unauthenticatedRedirect option, in that case it will transform those exceptions into redirects accordingly.
If you need to support both HTML and JSON requests/responses, then you can for example dynamically configure, respectively not configure the unauthenticatedRedirect option, based on the current request, eg in your Application::getAuthenticationService() method do something along the lines of:
$service = new AuthenticationService();
$accepts = array_map('trim', explode(',', $request->getHeaderLine('Accept')));
$isJsonRequest = in_array('application/json', $accepts, true);
if (!$isJsonRequest) {
// service config for non-JSON requests
$service->setConfig([
'unauthenticatedRedirect' => /* ...*/,
'queryParam' => 'redirect',
]);
}
Alternatively to evaluating the header manually, require the request to be an instance of \Cake\Http\ServerRequest and use its is() method:
assert($request instanceof \Cake\Http\ServerRequest);
if (!$request->is('json')) {
$service->setConfig([
'unauthenticatedRedirect' => [/* ...*/],
'queryParam' => 'redirect',
]);
}
Also note that the authentication component will by default require the identity to be present and throw an exception accordingly, you do not have to do that yourself.
Related
I have been trying to send some data to a controller via AJAX but for the life of me I can`t seem to make it work; everytime I make the request, a 403 forbidden error is thrown.
this is the ajax request:
$.ajax({
type: 'post',
url:"<?php echo Router::url(array('controller'=>'Atls','action'=>'saveTime', '_ext' => 'json'));?>",
dataType: 'json',
data: {atl_id: idTimerPaused, time: actual_time},
beforeSend: function(xhr){
},
success: function (response) {
console.log('Nailed It');
},
error: function(jqXHR, exception){
console.log(jqXHR);
}
});
return false;
the controller action:
public function saveTime()
{
if ($this->request->is('post') && $this->request->is('ajax')) {
$content = $this->request->getData();
$query = $this->Atls->query();
$result = $query
->update()
->set(
$query->newExpr('actual_time = '. $content['time'])
)
->where([
'id' => $content['atl_id']
])
->execute();
$this->set(compact('content'));
$this->set('_serialize', ['content']);
$this->render('ajax_response', 'ajax');
}
}
I have loaded the extensions on the routes.php file (Router::extensions('json', 'xml');)
The request handler is also loaded and the function is allowed:
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
}
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow('saveTime');
//Change layout for Ajax requests
$this->viewBuilder()->layout('appTemplate');
if ($this->request->is('ajax')) {
$this->viewBuilder()->layout('ajax');
}
}
that "ajax_response" view has also been added.
I can't see where the problem could be. So any help I can get to work this out would be much appreciated.
Did you use the 'Csrf'-Component? In my case this was the problem.
https://book.cakephp.org/3.0/en/controllers/components/csrf.html#csrf-protection-and-ajax-requests
When you got an 403 forbidden error in most cases the session is expired and the user has to login again.
i have set up the normal CSRF stuff etc and would work well, but then when i go and use ajax using the whatwg-fetch api (https://github.com/github/fetch)
Now all seem ok and all works well to what i have. BUT! Then I add the CSRF settings as below and it fails, all the time:
So, I have used the normal, but it fails and in the header I get the message: Failed CSRF check!
$app->add(new \Slim\Csrf\Guard);
But I wanted to add own message etc so have added the following, but still it does not pass.
$container['csrf'] = function ($c) {
$guard = new \Slim\Csrf\Guard;
$guard->setFailureCallable(function ($request, $response, $next) {
$request = $request->withAttribute("csrf_status", false);
return $next($request, $response);
});
return $guard;
};
$app->add($container->get('csrf'));
Then in my class i check it with:
if (false === $req->getAttribute('csrf_status')) {...}else{//all ok}
But what ever happens it always fails.
in my js i am adding the token details to the request like:
fetch('/post/url',{
method: 'POST',
headers: {
'X-CSRF-Token': {
'csrf_name':csrf_name,
'csrf_value':csrf_value
}
},
body: new FormData(theForm)
i have looked in the posted data etc and the form data is submitted including the csrf values etc. SO the require csrf data is being sent via the form as well as the header?
So how can I get the ajax functionality to work with the Slim CSRF, what am I missing?
Thanks in advance
I was also unable to get fetch to put the tokens into the body. I decided to extend the class so I could modify the __invoke method. I have added some code to pull the csrf from the headers.
in your dependencies now use this class.
$c['csrf'] = function ($c) {
return new \Foo\CSRF\Guard;
};
The extended class.
<?php
namespace MYOWN\CSRF;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* CSRF protection middleware.
*/
class Guard extends \Slim\Csrf\Guard
{
public function __construct(
$prefix = 'csrf',
&$storage = null,
callable $failureCallable = null,
$storageLimit = 200,
$strength = 16,
$persistentTokenMode = false
) {
parent::__construct(
$prefix,
$storage,
$failureCallable,
$storageLimit,
$strength,
$persistentTokenMode);
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
$this->validateStorage();
// Validate POST, PUT, DELETE, PATCH requests
if (in_array($request->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) {
$body = $request->getParsedBody();
$body = $body ? (array)$body : [];
$name = isset($body[$this->prefix . '_name']) ? $body[$this->prefix . '_name'] : false;
$value = isset($body[$this->prefix . '_value']) ? $body[$this->prefix . '_value'] : false;
if (!empty($csrfTokens = $request->getHeader('x-csrf-token'))) {
$csrfTokens = json_decode($csrfTokens[0], true);
$name = isset($csrfTokens[$this->prefix . '_name']) ? $csrfTokens[$this->prefix . '_name'] : false;
$value = isset($csrfTokens[$this->prefix . '_value']) ? $csrfTokens[$this->prefix . '_value'] : false;
}
if (!$name || !$value || !$this->validateToken($name, $value)) {
// Need to regenerate a new token, as the validateToken removed the current one.
$request = $this->generateNewToken($request);
$failureCallable = $this->getFailureCallable();
return $failureCallable($request, $response, $next);
}
}
// Generate new CSRF token if persistentTokenMode is false, or if a valid keyPair has not yet been stored
if (!$this->persistentTokenMode || !$this->loadLastKeyPair()) {
$request = $this->generateNewToken($request);
}
// Enforce the storage limit
$this->enforceStorageLimit();
return $next($request, $response);
}
}
well after several attempts over the last day and narrowing it down to the fetch api was using I decided to go back to the trusted jQuery aJax methods, and this seems to have worked.
Seems the following body and the new FormData() was not being picked up:
fetch('/post/url',{
method: 'POST',
body: new FormData(theForm)
So switched it out for
$.ajax({
url : '/url/to/post',
type: "POST",
data: {key:value, kay:value}
And all worked well.
The next issue to look into then is the keys being refreshed on first ajax call, preventing anymore calls unless page is refreshed, but thats for another day
I had another go at this after reading one of the blogs from one of the creators. So you can ignore my previous answer.
Sending the csrf in the body with these headers passes the csrf check.
const data = {
'csrf_name': csrf_name,
'csrf_value': csrf_value,
};
fetch(apiUrl, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json, application/x-www-form-urlencoded; charset=utf-8',
},
}).then((response) => {
if (response.ok) {
return response.json();
}
return null;
}).then((json) => {
console.log(json);
}).catch(() => {
});
What finally helped me succeed with Slim PHP and the CSRF values while using fetch was adding credentials: 'include' to the fetch request, like:
const body = JSON.stringify({
csrf_name: csrfName.value,
csrf_value: csrfValue.value
// You can add more data here
});
fetch('/some/request', {
method: 'POST',
body: body,
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
}).then(...)
I am using laravel 5.
I have a custom action in my controller. By custom I mean it is not used by the resource object in angular. The following is the code of my controller.
class ServicesController extends Controller {
public function __construct()
{
$this->middleware('guest');
}
public function extras()
{
// code here
}
}
This is my service code in the angular script.
(function() {
'use strict';
angular
.module('bam')
.factory('myservice', myservice);
function myservice($resource) {
// ngResource call to the API for the users
var Serviceb = $resource('services', {}, {
update: {
method: 'PUT'
},
extras: {
method: 'GET',
action: 'extras'
}
});
function getExtras(){
return Serviceb.query().$promise.then(function(results) {
return results;
}, function(error) {
console.log(error);
});
}
}
})();
Now, the query() here will send the request to the index method in the laravel controller. How will I access the extras() action in the getExtras() method?
It looks like you're almost there try out the example below I tried to use what you have in your question, and added a few other custom endpoints as examples. You'll want a base URL set up similarly to the example so you can feed it an id out of your payload so $resource can set up your base CRUD. Otherwise to make custom routes using the same resource endpoint you can add some extra actions like you have in your question, but apply your customization on the base endpoints URL.
.factory('ServicesResource', ['$resource',
function ($resource) {
// Parameters used in URL if found in payload
var paramDefaults = {
id: '#id',
param: '#param'
}
// Additional RESTful endpoints above base CRUD already in $resource
var actions = {
custom1: {
method: 'GET',
url: '/api/services/custom',
},
custom2: {
method: 'GET',
url: '/api/services/custom/:param',
},
extras: {
method: 'GET',
url: '/api/services/extras'
}
update: {
method: 'PUT'
}
}
// Default URL for base CRUD endpoints like get, save, etc
return $resource('/api/services/:id', paramDefaults, actions);
}])
Now you can dependency inject the factory and use it like this:
var payload = {param:'someParam'};
ServicesResource.custom(payload).$promise.then(function(response){
// handle success
}, function(reason) {
// handle error
});
Or for Extras:
ServicesResource.extras().$promise.then(function(response){
// Handle success
}, function(reason) {
// Handle error
});
In Laravel you're route might be something like this:
Route::get('services/{param}/custom', 'ServicesController#custom');
Or for extras like this:
Route::get('services/extras', 'ServicesController#extras');
I got what I wanted using $http.
function getExtras(){
return $http.get('/services/extras').success(function (results) {
return results;
});
}
But, that would be nice if anyone suggest me how to do it with Serviceb.query().$promise.then.
I am new to the world of Kohana/php and am having some issues understanding how to get the result of an ajax request. This request is being called from a click action and is invoking the following method.
function addClickHandlerAjax(marker, l){
google.maps.event.addListener(marker, "click",function(){
console.log(l.facilityId);
removeInfoWindow();
//get content via ajax
$.ajax({
url: 'map/getInfoWindow',
type: 'get',
data: {'facilityID': l.facilityId },
success: function(data, status) {
if(data == "ok") {
console.log('ok');
}
},
error: function(xhr, desc, err) {
console.log(xhr);
console.log("Details: " + desc + "\nError:" + err);
}
}); // end ajax call
});
}
Inside of my controller I have a method
public function action_getInfoWindow(){
if ($this->request->current()->method() === HTTP_Request::GET) {
$data = array(
'facility' => 'derp',
);
// JSON response
$this->auto_render = false;
$this->request->response = json_encode($data);
}
}
I see an HTTP request in fiddler and it passed the correct facilityID parameter. However I am having some disconnect about how to connect all the pieces together.
To send a response to the browser, you should user Controller::response instead of Controller::request::response. So your code should look like this:
public function action_getInfoWindow() {
// retrieve data
$this->response->body(json_encode($data));
}
That should give you some output.
Checkout the Documentation for more detailed info.
Edit
What you could do, to make your life a bit easier, especially if you gonna use ajax requests a lot, is to create an Ajax controller. You can stuff all checks and transforms in it, and never worry about it anymore. An example controller could look like below. Also checkout the Controller_Template which is shipped as an example by Kohana.
class Controller_Ajax extends Controller {
function before() {
if( ! $this->request->is_ajax() ) {
throw Kohana_Exception::factory(500, 'Expecting an Ajax request');
}
}
private $_content;
function content($content = null) {
if( is_null( $content ) ) {
return $this->_content;
}
$this->_content = $content;
}
function after() {
$this->response->body( json_encode( $this->_content ) );
}
}
// example usage
class Controller_Home extends Controller_Ajax {
public function action_getInfoWindow() {
// retrieve the data
$this->content( $data );
}
}
When try to delete a record using Yii Rest api
HTTP DELETE mothod is not working
my url routing is
array('users/list', 'pattern'=>'users/<model:\w+>/<id:\d+>', 'verb'=>'DELETE'),
when i request like
http://localhost/api/users/delete?id=1
in method DELETE in rest client
it says there is no method like delete
so i created an action like the following
public function actionDelete()
{
switch($_GET['action'])
{
case 'delete': // {{{
$id = $_GET['id'];
$this->DeleteUser($id);
break; // }}}
default:
break;
}
}
Now it says undefined index : model
My understanding is if we use HTTP DELETE method we should have an action called actionDelete right?
How to fix this ?
verb => 'DELETE' means that the request method is delete.
You should be able to call your api with the following javascript:
$.ajax({
url: 'index.php/api/users/1',
type: 'DELETE',
async: false,
success: function(result) {
alert('Deleted');
},
error: function(result) {
alert('Could not delete!');
}
});
Do you have the standard filters active? If yes, you should remove "postOnly + delete".
/**
* #return array action filters
*/
public function filters()
{
return array(
'accessControl', // perform access control for CRUD operations
'postOnly + delete', // we only allow deletion via POST request
);
}