When I delete an entity, cakephp warns that the request did not apply authorization checks. When I go back to previous action, the entity is deleted. Somehow it happens by bypassing the authorization middleware.
PS
I'm using a skeleton app from composer. I have not applied authorization to the delete action. I expected the delete to fail but it persists.
CakePHP 4
Controller code.
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$product = $this->Products->get($id);
if ($this->Products->delete($product)) {
$this->Flash->success(__('The product has been deleted.'));
} else {
$this->Flash->error(__('The product could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
Your code isn't bypassing authorization, it simply doesn't apply any authorization checks.
Authorization doesn't happen automatically, unless you explicitly configure/setup something that automatically issues checks, like for example the request authorization middleware, or model and action based authorization. If you don't have something like that configured, you have to issue authorization checks manually where required.
The message that no checks have been applied is primarily a debugging aid, the check happens in the middleware after your controller code already ran (it's not really possible to check it earlier, as your code is free to apply authorization checks at pretty much any point in your code).
If you explicitly want an action to not require authorization, then you can notify the component about it, by calling its skipAuthorization() method:
$this->Authorization->skipAuthorization();
See also
Authorization Cookbook > Checking Authorization
Authorization Cookbook > AuthorizationComponent > Automatic authorization checks
Authorization Cookbook > AuthorizationComponent > Skipping Authorization
Related
Has anyone an idear what to use as a general Authorization Service and have an working code example or good implementation steps how to implement such of thing.
It takes a lot of time to look what I am after, but didn't found any satisfied solution yet.
IdentityServer is not an option, while my permissions can not be stored as claims, because of the size of the token. It comes with about 200 persmissions, so it should be done in a dbcontext or something.
I looked at the PolicyServer, but it wasn't working as I expected. When I installed it at the IS4 application, it works on the IS4 controllers, but when the Authorize is called from an external application, it doesn't call the Authorize override at all were it should check the permissions.
And it seems that the permissions aren't set in the external application either in the User.Claims or what so ever. I'm missing some settings I think.
What I want to accomplish is that I have one permissions store (table) (which for example contains a bunch of index, add, edit or delete button or what so ever). The should be given to the autheniticated user which is logged in. But this single persmission-store should be available at all applications or APIs I run, so that the Authorize attribute can do his job.
I think it shouldn't be so hard to do, so I'm missing a good working example how to implement something like this and what is working.
Who can help me with this to get this done?
I wrote some code to get the permissions by API call and use that in the IsInRole override. But when I declare it with the Authorize attr, it will not get in the method:
[ApiController]
1) [Authorize]
public class AuthController : ControllerBase
{
private readonly IdentityContext _context;
public AuthController(IdentityContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
[HttpGet()]
[Route("api/auth/isinrole")]
public bool IsInRole(string role)
{
2) if (User.FindFirst("sub")?.Value != null)
{
var userID = Guid.Parse(User.FindFirst("sub")?.Value);
if([This is the code that checks if user has role])
return true;
}
return false;
This is the IsInRole override (ClaimsPrincipal.IsInRole override):
public override bool IsInRole(string role)
{
var httpClient = _httpClientFactory.CreateClient("AuthClient");
3) var accessToken = _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken).Result;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var request = new HttpRequestMessage(HttpMethod.Get, "/api/auth/isinrole/?id=" + role);
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
etc...
This isn't working while it is not sending the access_token in the request
The 'sub' isn't send
Is always null
The open source version of the PolicyServer is a local implementation. All it does is read the permissions from a store (in the sample a config file) and transform them into claims using middleware.
In order to use the permissions you'll have to add this middleware in all projects where you want to use the permissions.
Having local permissions, you can't have conflicts with other resources. E.g. being an admin in api1 doesn't mean you are admin in api2 as well.
But decentralized permissions may be hard to maintain. That's why you probably want a central server for permissions, where the store actually calls the policy server rather than read the permissions from a local config file.
For that you'll need to add a discriminator in order to distinguish between resources. I use scopes, because that's the one thing that both the client and the resource share.
It also keeps the response small, you only have to request the permissions for a certain scope instead of all permissions.
The alternative is to use IdentityServer as-is. But instead of JWT tokens use reference tokens.
The random string is a lot shorter, but requires the client and / or resource to request the permissions by sending the reference token to the IdentityServer. This may be close to how the PolicyServer works, but with less control on the response.
There is an alternative to your solution and that is to use a referense token instead of a JWT-token. A reference token is just an opaque identifier and when a client receives this token, he has go to and look up the real token and details via the backend. The reference token does not contain any information. Its just a lookup identifier that the client can use against IdentiyServer
By using this your tokens will be very small.
Using reference token is just one option available to you.
see the documentation about Reference Tokens
I used AuthComponent a lot but am new to AuthenticationMiddleware. I follow almost exactly https://book.cakephp.org/authentication/2/en/index.html except the username field is username instead of email. But when I try to get a page requiring authentication, Cake throws UnauthenticatedException in AuthenticationComponent::doIdentityCheck(). This can be understood because Cake should have redirected me to /users/login, which it did not. I tried
$service->loadAuthenticator('Authentication.Form', [
'loginUrl' => '/users/login' // case 1
// or case 2 'loginUrl' => \Cake\Routing\Router::url('/users/login'),
// or case 3 omit this to use the default
In all the above cases Cakes throws UnauthenticatedException with the message:
No identity found. You can skip this check by configuring requireIdentity to be false.
Also http://localhost:8765/users/login leads me to the correct page, also I can see the list of users by http://localhost:8765/users/ if I allow unauthenticated as this:
// UsersController.php
public function initialize() : void {
parent::initialize();
$this->Authentication->allowUnauthenticated(['index', 'login']);
}
My environment: CakePHP 4.0, authentication plugin 2.0. Is there a way to verify that AuthenticationMiddleware has indeed been set up and added to the middleware queue?
You'd get a different error if the middleware didn't ran, one that would complain about the authentication attribute missing on the request object.
You need to configure the unauthenticatedRedirect option on the service object in order for redirects being issued for unauthenticated requests, without that option being configured you'll receive exceptions instead. Additionally you may need to set the queryParam option (that's the query string parameter that will hold the initially accessed URL) if you want to be able to redirect users after successfully logging in.
$service->setConfig([
'unauthenticatedRedirect' => '/users/login',
'queryParam' => 'redirect',
]);
That seems to be missing from the docs, it's only mentioned in the migration notes. You may want to open an issue for that over at GitHub. The quickstart guide example has been updated to include the redirect configuration.
I am following the Implicit Workflow example from the angular-oauth2-oidc documentation.
Everything works well in my Angular app, and I can login (during which I am redirected to Identity Server), get my token and use this token to access my Web Api.
However, I have noticed that the "given_name" claim is null, and therefore, the username is not displayed on the login page. Specifically, the following method from the sample code appears to return null:
public get name() {
let claims = this.oauthService.getIdentityClaims();
if (!claims) return null;
return claims.given_name;
}
I thought perhaps this was a problem with permissions, but my scope is set to:
scope: 'openid profile email api1',
Any idea what I need to change to get this "given_name" claim?
For those who encountered the same issue. You can fix it by adding this line AlwaysIncludeuserClaimsInIdToken=true in the client settings on identity provider side.
OauthService.getIdentityClaims() is a Promise and holds UserInfo you can extract the name field with braces, so your function should be:
public get name() {
let claims = this.oauthService.getIdentityClaims();
if (!claims) return null;
return claims['name'];
}
The answer marked as "Best answer" is not correct. Get the user claims in the 'idtoken' will cause that the 'idtoken' be very big and then you may exceed the size limit.
The correct implementation is to use the 'UserInfo' Endpoint and then use the method 'loadUserProfile':
Example:
getUserClaims() {
const user = this.oauthService.loadUserProfile();
console.log(user, user);
}
I had the same issue, in my case with an error displayed on the browser console, saying that Request was blocked by Security Policy.
even having the AllowAnyOrigin() method called in startup, I lacked to get the header allowed. So when in my angular aap i call the loadUserProfile method via the
token_received event, it sends some headers that were not allowed.
Finaly this fix my issue:
app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader());
Don't forget calling that before usemvc
Is it possible to somehow extend IdentityServer4 to run custom authentication logic? I have the requirement to validate credentials against a couple of existing custom identity systems and struggle to find an extension point to do so (they use custom protocols).
All of these existing systems have the concept on an API key which the client side knows. The IdentityServer job should now be to validate this API key and also extract some existing claims from the system.
I imagine to do something like this:
POST /connect/token
custom_provider_name=my_custom_provider_1&
custom_provider_api_key=secret_api_key
Then I do my logic to call my_custom_provider_1, validate the API key, get the claims and pass them back to the IdentityServer flow to do the rest.
Is this possible?
I'm assuming you have control over the clients, and the requests they make, so you can make the appropriate calls to your Identity Server.
It is possible to use custom authentication logic, after all that is what the ResourceOwnerPassword flow is all about: the client passes information to the Connect/token endpoint and you write code to decide what that information means and decide whether this is enough to authenticate that client. You'll definitely be going off the beaten track to do what you want though, because convention says that the information the client passes is a username and a password.
In your Startup.ConfigureServices you will need to add your own implementation of an IResourceOwnerPasswordValidator, kind of like this:
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
Then in the ValidateAsync method of that class you can do whatever logic you like to decide whether to set the context.Result to a successful GrantValidationResult, or a failed one. One thing that can help you in that method, is that the ResourceOwnerPasswordValidationContext has access to the raw request. So any custom fields you add into the original call to the connect/token endpoint will be available to you. This is where you could add your custom fields (provider name, api key etc).
Good luck!
EDIT: The above could work, but is really abusing a standard grant/flow. Much better is the approach found by the OP to use the IExtensionGrantValidator interface to roll your own grant type and authentication logic. For example:
Call from client to identity server:
POST /connect/token
grant_type=my_crap_grant&
scope=my_desired_scope&
rhubarb=true&
custard=true&
music=ska
Register your extension grant with DI:
services.AddTransient<IExtensionGrantValidator, MyCrapGrantValidator>();
And implement your grant validator:
public class MyCrapGrantValidator : IExtensionGrantValidator
{
// your custom grant needs a name, used in the Post to /connect/token
public string GrantType => "my_crap_grant";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// Get the values for the data you expect to be used for your custom grant type
var rhubarb = context.Request.Raw.Get("rhubarb");
var custard = context.Request.Raw.Get("custard");
var music = context.Request.Raw.Get("music");
if (string.IsNullOrWhiteSpace(rhubarb)||string.IsNullOrWhiteSpace(custard)||string.IsNullOrWhiteSpace(music)
{
// this request doesn't have the data we'd expect for our grant type
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return Task.FromResult(false);
}
// Do your logic to work out, based on the data provided, whether
// this request is valid or not
if (bool.Parse(rhubarb) && bool.Parse(custard) && music=="ska")
{
// This grant gives access to any client that simply makes a
// request with rhubarb and custard both true, and has music
// equal to ska. You should do better and involve databases and
// other technical things
var sub = "ThisIsNotGoodSub";
context.Result = new GrantValidationResult(sub,"my_crap_grant");
Task.FromResult(0);
}
// Otherwise they're unauthorised
context.Result = new GrantValidationResult(TokenRequestErrors.UnauthorizedClient);
return Task.FromResult(false);
}
}
I have two separate projects: UI(AngularJS) and Server(Symfony2).
I need to send cross-domain PUT request from AngularJS application to the Symfony2 app.
In the Symfony controller I passed $request to the form->handleRequest(); and debug showed me, that form using this way is not submitted.
So, next I tried to pass $request to the form->submit() and got error "Invalid CSRF Token".
How can I correctly process cross-domain data via Symfony forms?
I've read that passing $request to the submit() method is
depricated.
How can I pass CSRF token to the form if I send it from UI via
headers ? (I add csrf field to the request but it not processing at back-end)
EDITED: Currently I see that issue is related to CSRF token. No matter how I sending this token from UI, it's not processed on back-end and I always get "Invalid CSRF token" error.
I tried to add _token field directly to json object and set field name to _token via csrf_field_name option into my formtype class.
I also tried to pass json_decode($request->getContent(), true) to my form submit() method, but after debugging I see, that submittedData is changing in next code :
// Symfony/Component/Form/Form.php, submit() method
if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
// here submittedData has csrf _token key/value
$event = new FormEvent($this, $submittedData);
$dispatcher->dispatch(FormEvents::PRE_SUBMIT, $event);
$submittedData = $event->getData();
// now submittedData without _token key/value
}
EDITED2: more details. CsrfValidationListener that using by Symfony Form component call $this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName])) and this return false, the issue in next code:
// Symfony/Component/Security/Csrf/CsrfTokenManager.php
public function isTokenValid(CsrfToken $token)
{
if (!$this->storage->hasToken($token->getId())) {
return false;
}
return StringUtils::equals($this->storage->getToken($token->getId()), $token->getValue());
}
It seems csrf token is stored into session, so isTokenValid() method return false.
I continue to debug.
EDITED3:
as I can see, session is empty on calling $this->storage->hasToken($token->getId()) from CsrfTokenManager.php.
This is very strange, because I generate csrf token from my controller in next way:
$csrfToken = $this->get('security.csrf.token_manager')->refreshToken('Symfony');
And as I can see, refreshToken() method save csrf token into db:
// Csrf/CsrfTokenManager.php
public function refreshToken($tokenId)
{
$value = $this->generator->generateToken();
$this->storage->setToken($tokenId, $value);
return new CsrfToken($tokenId, $value);
}
// Csrf/TokenStorage/SessionTokenStorage.php
public function setToken($tokenId, $token)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
$this->session->set($this->namespace.'/'.$tokenId, (string) $token);
}
But when I send data to the form, $this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName])) that calls from preSubmit() method of CsrfValidationListener return empty session.
just in case I add my security.yml settings, maybe I have missed something:
main:
pattern: ^/(?!login).+
stateless: true
simple_preauth:
authenticator: app_bundle.api_key_authenticator
provider: api_key_user_provider
anonymous: ~
logout: ~
login:
pattern: ^/login
stateless: false
simple_preauth:
authenticator: app_bundle.email_password_authenticator
provider: email_user_provider
anonymous: ~
Notice: I generate csrf-token under login firewall and try to access it from main firewall!
But I also tried to generate csrf-token in the same firewall. Nothing changed.
EDITED4:
I have configured custom session dir for tracking session creation. So, I can see, that on login I have session with all attributes, but when I doing PUT request, I notice that new session file is created and it contains something like this:
_sf2_attributes|a:0:{}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1449700968;s:1:"c";i:1449700968;s:1:"l";s:1:"0";}
Just empty session.
So, I have found the reason of csrf error behavior.
When csrf token created, it stored into session. Because of my api firewall is stateless, it can't store csrf token. And also on each authentication session is drop and has only current authentication token.
Properly configured CORS help to protect from csrf attack.
See also this answer about CORS headers.
Hope, this will be helpful for somebody.