How to restrict saving of data in db for user when the admin deactivated it in cakephp3.x? - authorization

I need a solution where I can restrict the user to save any data when he is been deactivated by the admin. Suppose the user is active on a page where he is going to save a form but at the same instance admin has deactivated him. so , now when he try to save the form , he should be redirected to the login page saying "Your account is been deactivated, contact the support", without saving the data. I am working in cakephp 3.x . I tried to use beforeFilter for it. But it is deactivating the user but also the user is able to save the data.

I had a similar situation. I added a custom finder to the auth component to restrict deactivated users from making requests when they were deactivated but it only stopped deactivated users from logging in and NOT immediately restricting them from making any request. This meant a deactivated user could still access the application for the remainder of their session. (A lot of havoc can be caused by a disgruntled deactivated employee in say 10 hours.)
My solution was to tell the auth component to use controller hook methods for authorization. Cookbook info here
App Controller - Initialize action - Auth Component
$this->loadComponent('Auth', [
'authorize' => 'Controller', // ADDED THIS LINE
'authenticate' => [
'Form' => [
'finder' => 'active' // Custom finder to retrieve details for login of active users - for login only.
]
],
// Other auth component actions here
]);
And this is what logs the user out immediately
App Controller - isAuthorized
public function isAuthorized()
{
if ($this->checkActiveAndRole() === true) {
return true;
}
return false;
}
App Controller - checkActiveAndRole - (Streamlined for this post)
private function checkActiveAndRole()
{
// Initialise and validate the id from auth component.
$id = $this->Auth->user('id');
// Select the users status and role.
$Users = TableRegistry::getTableLocator()->get('Users');
$query = $Users->find('statusRole', [
'id' => $id
]);
if ($query->isEmpty()) {
return false;
}
$status = 0;
foreach ($query as $row):
$status = $row->status;
endforeach;
// Check if the user is active.
if ($status === 0) {
return false;
}
return true;
}
And this worked for me. Ie: Checking if the user is active on every request with the isAuthorized() function in the app controller

Related

Why is the callback identifier not being invoked?

I'm trying to implement matching a Kerberos authentication with a local user database in CakePHP4. So I installed CakePHP 4 and the Authentication plugin 2.0. Since Kerberos auth is managed by our IIS WebServer, only thing I have to do is check if the authenticated user is known by my webapp.
The callback authentication should let me implement something like this, right ?
So I put this function in Application.php :
<?php
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$service = new AuthenticationService();
// Define where users should be redirected to when they are not authenticated
$service->setConfig([
'unauthenticatedRedirect' => '/users/login',
'queryParam' => 'redirect',
]);
// Load the authenticators. Session should be first.
$service->loadAuthenticator('Authentication.Session');
$service->loadIdentifier('Authentication.Callback', [
'callback' => function($data) {
// do identifier logic
if (empty($_SERVER['REMOTE_USER'])) {
return new Result(
null,
Result::FAILURE_OTHER,
['message' => 'Unknown user.']
);
} else {
// On vérifie que l'utilisateur est autorisé à utiliser cette application
$users = TableRegistry::getTableLocator()->get('Users');
$remoteUserNoDomain = str_replace("DOMAIN\\", "", $_SERVER['REMOTE_USER']);
$result = $users->find()
->where(['username' => $remoteUserNoDomain]);
if ($result) {
return new Result($result, Result::SUCCESS);
}
return new Result(
null,
Result::FAILURE_OTHER,
['message' => 'Removed user.']
);
}
return null;
}
]);
return $service;
}
But so far, it doesn't seem to work, like it won't call the callback function at all. I tried to put some debug code, exits... Nothing works.
I would assume that you've also done all the other required configuring for authentication to work, ie loading the plugin, adding the authentication middleware, etc.!?
https://book.cakephp.org/authentication/2/en/index.html
That said, identifiers do not do any work on their own, they are being triggered by authenticators in case they actually require them. You only have the Session authenticator loaded, which in its default configuration doesn't make use of identifiers, but even if you configure it to use identifiers (by setting its identify option to true), it will only use them when there already is an identity in the session, then the identifier is being used to validate that identity.
https://github.com/cakephp/authentication/blob/2.3.0/src/Authenticator/SessionAuthenticator.php#L52
I'm not familiar with Kerberos authentication, but if it pre-populates $_SERVER['REMOTE_USER'] (btw. never access superglobals in CakePHP directly, it will only cause trouble down the road), then what you need is a custom authenticator. You could then re-use the password identifier for the ORM access part, as it allows finding something without checking the password (weirdly enough, given its name).
Quick and dirty example based on your snippet:
// src/Authenticator/KerberosAuthenticator.php
namespace App\Authenticator;
use Authentication\Authenticator\AbstractAuthenticator;
use Authentication\Authenticator\Result;
use Authentication\Authenticator\ResultInterface;
use Psr\Http\Message\ServerRequestInterface;
class KerberosAuthenticator extends AbstractAuthenticator
{
public function authenticate(ServerRequestInterface $request): ResultInterface
{
$server = $request->getServerParams();
if (empty($server['REMOTE_USER'])) {
return new Result(null, Result::FAILURE_CREDENTIALS_MISSING);
}
$remoteUserNoDomain = str_replace("DOMAIN\\", "", $server['REMOTE_USER']);
$user = $this->_identifier->identify(['username' => $remoteUserNoDomain]);
if (empty($user)) {
return new Result(
null,
Result::FAILURE_IDENTITY_NOT_FOUND,
$this->_identifier->getErrors()
);
}
return new Result($user, Result::SUCCESS);
}
}
Your service authenticator/identifier setup would then look like this:
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Kerberos');
$service->loadIdentifier('Authentication.Password');
Nore sure if you'd then really want to use the session authenticator like that though, ie whether you only want to identify the remote user once per session.

How to allow to unauthenticated users see some specific pages or actions pages in Cake PHP 3?

With CakePHP 3 we used Auth component and this worked like this CakePHP - How to allow unauthenticated access to specific pages
Now I'm trying to use the new Authentication and Authorization plugins instead (I don't know if it is the best solution).
I have this case:
I have some tables in the database for entities (cars, brands, and users). I have users and 4 level user roles (pyramid).
- Admins can change everything
- Editors can see and add brands and cars, but only can edit or update cars and brands created by themselves.
- Registered users can add only cars and edit their cars (and see all cars and brands).
- Anonymous users can see all but only can create a user account.
Authentication works well alone. To allow anonymous user access to content I use $this->Authentication->allowUnauthenticated(['login', 'add']); but when I load Authorization plugin, everything give error.
Do I need to specify all Authorization access with authorizeModel and other functions? There is a way to authorize at the same time with both plugins? Do I really need Authorization plugin for this and is recommended or Authentication plugin can handle this?
With previous Auth component I worked with something like this piece of code:
In AppController.php
public function beforeFilter(Event $event)
{
$this->Auth->allow(['view', 'display']);
}
public function isAuthorized($user)
{
return true;
}
In UsersController.php
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow('add', 'logout');
}
In Cars and Brands controllers
public function isAuthorized($user)
{
if (isset($authUser['role']) && $authUser['role'] === 'admin') {
return true;
}
if ($this->request->action === 'add') {
return true;
}
if ($this->request->action === 'index') {
return true;
}
if (in_array($this->request->action, ['edit'])) {
$carId = (int)$this->request->params['pass'][0];
if ($this->Cars->exists(['id' => $carId, 'user_id' => $authUser['id']])) {
return true;
}
}
return false;
}
Followed from https://book.cakephp.org/3/es/tutorials-and-examples/blog-auth-example/auth.html
My versions are:
- CakePHP 3.8
- Authentication plugin 1.4
- Authorization plugin 1.3
Sorry if my question is a bit basic but documentation is not very clear with this. I can add more details if needed.
Edit: If I quit unauthenticatedRedirect I get:
No identity found. You can skip this check by configuring `requireIdentity` to be `false`.
Authentication\Authenticator\UnauthenticatedException
If I add requireItentity as false, in AppController
$this->loadComponent('Authentication.Authentication', [
'requireIdentity' => false
]);
I get (where / is the path, can be /cars /brands)
The request to `/` did not apply any authorization checks.
Authorization\Exception\AuthorizationRequiredException
If I use this in AppController (always Authentication before Authorization)
$this->loadComponent('Authentication.Authentication', [
'requireIdentity' => false
]);
$this->loadComponent('Authorization.Authorization', [
'skipAuthorization' => [
'login',
]
]);
and this in Application
$service->setConfig([
'unauthenticatedRedirect' => \Cake\Routing\Router::url('/users/login'),
'queryParam' => 'redirect',
]);
I send all users to login page but authorization checks error appears.
With $this->Authorization->skipAuthorization(); in beforeFilter() user can see the pages and works but I don't know if it is appropriated.
If I use this in any controller beforeFilter $this->Authorization->authorizeModel('index', 'add', 'display' ...);
I get
Policy for `App\Model\Table\CarsTable` has not been defined.
Authorization\Policy\Exception\MissingPolicyException
In home (or pages controller) I get
Policy for `Cake\ORM\Table` has not been defined.
Authorization\Policy\Exception\MissingPolicyException
Do I really need to create policies for each table? I think is more complex than previous Auth component or maybe I'm doing something wrong.

Implementing custom claims for an Angular5(AngularFire2)/Cloud Firestore app with Firebase Auth

I'm trying to implement custom claims in my Angular5(AngularFire2)/Cloud Firestore app with Firebase Auth. I want the following business rules to apply.
A cloud function updates all newly created firebase auth users from all sources(Google, email and password, Twitter, etc) to have custom claims set player is true and disabled is false.
exports.processSignUpNewUser = functions.auth.user().onCreate(event => {
const user = event.data; // The Firebase user.
// Check if user meets role criteria.
if (user.email &&
user.emailVerified) {
const customClaims = {
player: true,
disabled: false
};
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, customClaims)
.then(() => {
// Update real-time database to notify client to force refresh.
const metadataRef = admin.database().ref("metadata/" + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
return metadataRef.set({refreshTime: new Date().getTime()});
})
.catch(error => {
console.log(error);
});
}
});
This function example from the Firebase docs does something for the realtime database that I'm not sure applies to Cloud Firestore. What do I have to do differently for Cloud Firestore?
I want another cloud function to update the user document role section for display purposes
exports.createUserAddDefaultRoles = functions.firestore
.document('users/{userId}')
.onCreate
((event) => {
const data = event.data.data();
const roles = '{ "disabled": false, "player": true }';
return event.data.ref.set({
roles: roles
}, {merge: true});
How do I lock down the roles section of the user doc so only Cloud functions can update it and still allow the user to read the information on their profile and edit all the other properties of the user doc?
Is there a better function I could write to just update the roles section every time the user's custom claims change?
Question 1:
What the example does with the database is tell the client to refresh its authentication token so that the new claims work when making requests. Which basically means that when you update custom claims you have to tell the client to call firebase.auth().currentUser.getIdToken(true);.
Question 2:
You can only control access to a specific document, not document properties, so my recommendation would be to create a different document collection for the stuff you don't want the user to edit.
You need to update your firestore rules to:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userid}{
allow read, write, update: if request.auth.uid == userid
}
match /userClaims/{userid}{
allow read: if request.auth.uid == userid
}
}
}
Question 3:
Since custom claims can only be changed on the server, make sure every time you change them, you also change the firestore doc either by calling a firestore function or doing it in the same code.
Sources:
Firebase docs

How do I access the oauth2 authenticated user in a Lumen controller?

I have followed this excellent tutorial Building a Web App with Lumen and OAuth2 for setting up OAuth2 and Lumen. Everything is working fine apart from now I want to access the currently authenticated user information/model.
My route correctly posts the supplied information after I have logged in and I can break with Netbeans inside the controller but I am not clear how to get the user from the underlying Auth framework. I have tried the three methods indicated here Authentication - Laravel but to no avail. The lumen logs shows:
==== routes.php ====
$app->group(['namespace' => 'App\Http\Controllers','prefix' => 'api', 'middleware' => 'oauth'], function($app)
{
$app->post('info', 'InfoController#send');
}
==== InfoController.php ====
namespace App\Http\Controllers;
// the controllers
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Authenticatable;
class InfoController extends Controller{
/* /api/info methods */
public function send(Request $request){
// can iterate over the entire users table but I just
// want the current user (must be some method through
// the authentication stack)
$users = \App\Auth\User::all();
foreach ($users as $user) {
$name = $user->name;
$key = $user->getAuthIdentifier();
$pwd = $user->getAuthPassword();
}
// CODE GETS HERE BUT how to get the current user?
// Authenticated OK (request supplies "Access-Token: Bearer ...")
}
}
This is probably not the cleanest solution and may not match your requirements exactly but it does retrieve the user.
I decided to make another DB query in the proxy to get the user with the same key (in my case, email address) that was requested by the client.
In my case I was sending the user id along with the standard oauth token.
You could use the same technique to set some value in the session.
// ../app/Auth/Proxy.php
namespace App\Auth;
use App\User; // ----- added this line
use GuzzleHttp\Client;
class Proxy {
...
private function proxy($grantType, array $data = [])
{
...
$response = json_decode($guzzleResponse->getBody());
if (property_exists($response, "access_token")) {
...
// added the following line to get the user
$user = User::where('email',$data['username'])->get()->first();
// untested, but you could add the user to your session here
$request = app()->make('request');
$request->session()->put('current_user', $user);
$response = [
'accessToken' => $response->access_token,
'accessTokenExpiration' => $response->expires_in,
'userId' => $user->id,
];
}
...

How to override sfDoctrineGuard validators in order to allow login based on credentials

As title said I need to modify sfDoctrineGuard validators to allow login based on credentials. I've this users and credentials:
username credential
administrator super admin
user1 public_access
user2
user3 public_access
I've also two applications: admin and site. So based on this data I need to achieve this:
user1 and user3 could login in site app but not in admin
user2 couldn't login in site and either in admin
administrator could login in admin but not in site
Any help?
First you should copy the sfGuardValidatorUser class from plugins/sfDoctrineGuardPlugin/lib/validator into lib/validator and clear the cache.
Than modify it's doClean method to check if a user has the required permission.
class sfGuardValidatorUser extends sfValidatorBase
{
// ...
protected function doClean($values)
{
$username = isset($values[$this->getOption('username_field')]) ? $values[$this->getOption('username_field')] : '';
$password = isset($values[$this->getOption('password_field')]) ? $values[$this->getOption('password_field')] : '';
$callable = sfConfig::get('app_sf_guard_plugin_retrieve_by_username_callable');
if ($username && $callable)
{
$user = call_user_func_array($callable, array($username));
}
elseif ($username)
{
$user = $this->getTable()->retrieveByUsername($username);
}
else
{
$user = false;
}
/** #var $user sfGuardUser */
if ($user && $user->isAllowedToLogIn(sfConfig::get('sf_app'), $password))
{
return array_merge($values, array('user' => $user));
}
if ($this->getOption('throw_global_error'))
{
throw new sfValidatorError($this, 'invalid');
}
throw new sfValidatorErrorSchema($this, array($this->getOption('username_field') => new sfValidatorError($this, 'invalid')));
}
// ...
}
Than add isAllowedToLogIn method to the sfGuardUser class:
public function isAllowedToLogIn($app, $password)
{
if ('admin' == $app)
{
$hasPermission = $this->getIsSuperAdmin() || $this->hasPermission('admin');
}
elseif ('site' == $app)
{
$hasPermission = $this->hasPermission('public_access');
}
else
{
$hasPermission = true;
}
return $this->getIsActive() && $this->checkPassword($password) && $hasPermission;
}
Modify the permission check part if needed.
I recommend to add an admin group and permission as well and if you can don't use the is_super_admin flag.
You are mixing two things here - authentication and authorisation of users.
Authentication means that your users can log into the website, and you have a confirmation that this the right person.
Authorisation filters which content is available to which users (authenticated or not).
So what you are looking for here is to block access to users who should not be able to see the admin app, not block the login.
If you block the login part (using validators, your own signing action or whatever) you will only block anonymous users who will want to see your admin app. When they try to log in they will be rejected because they are not admins. But if the user logs in to the site app and then goes to admin there will no additional login required and the user will be granted access (unless you set the proper authorisation).
So what you need is to add a security.yml file to your apps/admin/config folder with this content:
all:
is_secure: true
credentials: super_admin
If a 'normal' user logs in and tries to navigate to admin app he will be denied access.