Laravel8 role based access using and pivot tables and middlewares - access-control

I am trying to write a large project using laravel8 and will need role based access. So I have created a roles table and linked it to the users table via a modal table called role_user. Now I am able to create the access and perform checks inside controllers and users model and everything works correctly
my problem is that this way I have to keep checcking if a user has access in each and every function for all my user access levels and this is tedious.
I have thus tried to convert this approcah and use middlewares but the problem is that I am unable to redirect the users to the appropriate dashboard upon authentication and they are both being redirected to the home page that is designated for users only.
I have tried the following
I have addes the following code to the users model to create a many to many relationship between the users and the roles and also to check if the user has roles and peform appropriate tasks as per the code blocks
public function roles() {
return $this->belongsToMany(Role::class);
}
public function checkRoles($roles) {
if ( ! is_array($roles)) {
$roles = [$roles];
return false;
}
if ( ! $this->hasAnyRole($roles)) {
auth()->logout();
abort(404);
}
}
public function hasAnyRole($roles): bool {
return (bool) $this->roles()->whereIn('name', $roles)->first();
}
public function hasRole($role): bool {
return (bool) $this->roles()->where('name', $role)->first();
}
Now this would have worked if I proceeded to peforem checks on each controller's model directly but after creating middlewares for each role and peforming checks in each as follows things failled
a) Admin Middleware
public function handle(Request $request, Closure $next){
if (Auth::user() && $request->user()->hasRole('admin')) {
return $next($request);
}
return redirect('home')->with('error','You have not admin access');
}
b) SuperAdmin Middleware
public function handle(Request $request, Closure $next) {
if (Auth::user() && $request->user()->hasRole('super_admin')) {
return $next($request);
}
return redirect('home')->with('error','You have not permission to peform this task');
}
Now after registering all the middlewareds inside the Kernel class, I modified the login Controller class and added the following code inside the construct function:
if(Auth::check()){
if(Auth::user()->hasRole('super_admin')){
return redirect(RouteServiceProvider::SUPERADMINADMINHOME);
}elseif(Auth::user()->hasRole('admin')){
return redirect(RouteServiceProvider::ADMINHOME);
}elseif(Auth::user()->hasRole('vendor')){
return redirect(RouteServiceProvider::VENDORHOME);
}else {
return redirect(RouteServiceProvider::HOME);
}
}
I also updated my routes as follows
Route::group(['prefix' => 'super', 'as' => 'super.', 'namespace' => 'Super', 'middleware' => ['Super']], function () {
Route::get('/', [App\Http\Controllers\SuperAdminController::class,'index'])->name('superadminhome');
});
Route::group(['prefix' => 'admin', 'as' => 'admin.', 'namespace' => 'Admin', 'middleware' => ['Admin']], function () {
Route::get('/', [App\Http\Controllers\AdminController::class,'index'])->name('adminhome');
});
Route::group(['prefix' => 'vendor', 'as' => 'vendor.', 'namespace' => 'Vendor', 'middleware' => ['Vendor']], function () {
Route::get('/', [App\Http\Controllers\VendorController::class,'index'])->name('vendorhome');
});
After doing all this, I tried tom login with the superadmin credentials and the admin credentials but all of them take me to the same page home. What could I be doing wrong or where can I get step by step guide to how to achieve this task noting that I am new to middlewares in laravel.

From what I can see you are having this problem becasue the checks are all yielding false hence the last is being taken into account. That would mean that the way in which you are checking for the roles is probably wrong. I would suggest looping through the roles and using a switch statement. checking if it matches the ones you need and then redirecting appropriately.
This would mean changing your if statements that check for the availability of a specific roles.
That is this part of your code
if(Auth::user()->hasRole('super_admin')){
return redirect(RouteServiceProvider::SUPERADMINADMINHOME);
}elseif(Auth::user()->hasRole('admin')){
return redirect(RouteServiceProvider::ADMINHOME);
}elseif(Auth::user()->hasRole('vendor')){
return redirect(RouteServiceProvider::VENDORHOME);
}else {
return redirect(RouteServiceProvider::HOME);
}

Related

cakephp3 entities relationship error when login (the error is going after refresh)

I am struggling with this issue for a few days. I've tried to debug step by step with Xdebug, but I cannot find where it is the problem.
Basically when login into the cakephp3.9 I get this error:
App\Model\Table\UsersTable association "Roles" of type "manyToMany" to "Slince\CakePermission\Model\Table\RolesTable" doesn't match the expected class "App\Model\Table\RolesTable".
You can't have an association of the same name with a different target "className" option anywhere in your app.
As I mentioned above, I am using cakephp 3.9 and the slince package ("slince/cakephp-permission": "^1.0") to manage roles/permissions. After get this error if I refresh the browser evertyhing works as normal. The error only appears once, always after login.
Relations in UsersTable.php
$this->belongsToMany('Roles', [
'foreignKey' => 'user_id',
'targetForeignKey' => 'role_id',
'joinTable' => 'users_roles'
]);
UsersController.php
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
if (Configure::read('Options.status') == 2) {
$this->Flash->error('Please confirm your account - click on the validation link emailed to you');
return $this->redirect(['action' => 'login', 'controller' => 'Users']);
}
$UsersRoles = TableRegistry::getTableLocator()->get('UsersRoles');
// Get User role_id
$AuthRole = $UsersRoles
->find()
->select(['role_id'])
->where(['user_id' => $user['id']])
->first();
// if the status of the user is false an error appears and it will be redirected back || check if is an admin role?
if ($user['status'] != 1 || $AuthRole->role_id > 3) {
$this->Flash->error('Your account is not authorized to access this area. Contact the support team or check your inbox');
return $this->redirect(['action' => 'login', 'controller' => 'Users']);
}
$Roles = TableRegistry::getTableLocator()->get('Roles');
// Get Role name
$AuthRoleName = $Roles
->find()
->select('name')
->where(['id' => $AuthRole['role_id']])
->first();
$user['role_id'] = $AuthRole['role_id'];
$user['role_name'] = $AuthRoleName['name'];
// Set the use into the session
$this->Auth->setUser($user);
// Save the previous login date to the session and enable tour vars
$session = $this->getRequest()->getSession();
if (empty($user['last_login'])) {
$session->write('Options.run', true);
$session->write('Options.player', true);
}
// Now update the actual login time
$this->Users->updateLastLogin($this->Auth->user('id'));
// Handle case where referrer is cleared/reset
$nextUrl = $this->Auth->redirectUrl();
if ($nextUrl === "/") {
return $this->redirect(['action' => 'index', 'controller' => 'Adminarea']);
} else {
return $this->redirect($nextUrl);
}
}
$this->Flash->error(__('Invalid username or password, please try again'));
}
$this->viewBuilder()->setLayout('admin_in');
}
The issue it is in the relationship "Roles", it already exists in the file "PermissionsTableTrait.php" from the slice package, and it seems that cannot be two relationships with the same name.

CakePHP3 BeforeFilter & Auth Redirect

Can anyone help me understand the deal with Cakephp 3.3 and a BeforeFilter/Auth Redirect issue I'm experiencing.
I'm using the default Auth component. I've created a custom component that additionally checks for a session variable (Registration), and if that variable is not set redirects to a page designed to make a selection to set the desired Registration.
Here's my custom component:
<?php
namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Network\Request;
class RegistrationCheckComponent extends Component
{
private $_allowedActions = [];
private $_superUserBypass = false;
public $components = ['Auth'];
public function superUserBypass($val = false) {
$this->_superUserBypass = $val;
}
public function allow(Array $allowedActions = []) {
$this->_allowedActions = $allowedActions;
}
public function verify() {
if($this->_superUserBypass) {
return true;
}
$session = $this->request->session();
//if Auth Registration is not set
if(!$session->read('Auth.Registration')) {
//if requested action is not in the array of allowed actions, redirect to select registration
if(!in_array($this->request->param('action'), $this->_allowedActions)) {
return $this->redirect();
};
return true;
}
return true;
}
public function redirect() {
$controller = $this->_registry->getController();
return $controller->redirect($this->config('redirect'));
}
}
Not all controller's require the Registration variable to be set, that's why I decided to go with the component approach. The component is however loaded in the AppController by this line:
$this->loadComponent('RegistrationCheck', ['redirect' => ['controller' => 'Users', 'action' => 'registrations']]);
In the controllers that require the Registration variable to be set, I include the following beforeFilter function:
public function beforeFilter(Event $event) {
parent::beforeFilter($event);
return $this->RegistrationCheck->verify();
}
Now, I've had some Integration Tests defined, here's one of them:
public function testUnauthenticatedEdit()
{
$this->get('/teams/edit');
$this->assertRedirect(['controller' => 'Users', 'action' => 'login']);
}
So, after I implemented my RegistrationCheck component, I ran the Integration Tests. I was expecting the test to pass, it did not. The interesting thing is that it actually returned a redirect to Users->registrations rather than Users->login as I had expected.
It looks to me that the RegistrationCheck redirect is happening before the Auth component redirect. I'm not sure it's a huge deal, because a redirect to registrations without Auth set is going to end up redirecting back to login, but it seems incorrect to ignore it...also, I'd just like to understand a bit more of what is actually going on.
Can anyone suggest changes to my code that would ensure the Auth component is handled before the RegistrationCheck component?
Thanks in advance.
Well, after a little more research, I found the answer I'm looking for here: http://book.cakephp.org/3.0/en/controllers/components/authentication.html#deciding-when-to-run-authentication
Pretty simple really, just wanted to include an answer here for anyone who may stumble across the same question.

How to call a function within the same controller?

I have to call a soap service using laravel and done so correctly. This soap service requires me to send a login request prior to sending any other request.
The code I'm using works, but I want to improve by removing the login from all the functions and creating one function.
I tried changing the following for one function:
public function getcard($cardid)
{
SoapWrapper::add(function ($service) {
$service
->name('IS')
->wsdl(app_path().'\giftcard.wsdl')
->trace(true);
});
$data = [
'UserName' => 'xxxx',
'Password' => 'xxxx',
];
$card = [
'CardId' => $cardid,
];
SoapWrapper::service('IS', function ($service) use ($data,$card) {
$service->call('Login', [$data]);
$cardinfo=$service->call('GetCard', [$card]);
dd($cardinfo->Card);
});
}
Into:
public function login()
{
SoapWrapper::add(function ($service) {
$service
->name('IS')
->wsdl(app_path().'\giftcard.wsdl')
->trace(true);
});
$data = [
'UserName' => 'xxxx',
'Password' => 'xxxx',
];
SoapWrapper::service('IS', function ($service) use ($data) {
return $service->call('Login', [$data]);
//$service->call('Login', [$data]);
//return $service;
});
}
public function getcard($cardid)
{
$this->login();
$card = [
'CardId' => $cardid,
];
$cardinfo=$service->call('GetCard', [$card]);
dd($card);
}
But this doesn't work. I also tried it with the commented out part, but that doesn't work. Both options result in an error that it didn't find 'service'.
I know it has something to do with oop, but don't know any other option.
I took this as an example, but I probably implemented it wrong?
So my question is: How do I reuse the login part for all other functions?
Your return statement in the login() method is within the scope of that closure. You need to return the result of the closure as well.
return SoapWrapper::service('IS', function ($service) use ($data) {
return $service->call('Login', [$data]);
});
EDIT:
To explain a little bit. You have a function:
SoapWrapper::service('IS' ,function() {}
Inside of a function : public function login()
If you need to return data from your login() method, and that data is contained within your SoapWrapper::service() method, then both methods need a return statement

Laravel 5 customizing authentication

In Laravel 5 how can you customize the default authentication which comes out of the box? For example I have multiple types of users to authenticate against. Each type of user is defined by a role i.e Jobseeker, Recruiter etc. Each type of user will have a different type of registration form to capture some of the profile details as well. So I have the following tables:
users
roles
role_user
jobseeker_profile
recruiter_profile
The default authcontroller and passwordcontroller in Laravel 5 uses traits for all the authentication methods. How would you go about customizing it - do you guys edit the existing trait files? So for example the getRegister method returns the register view but I would want it to check the route before deciding which view to show.
// default method
public function getRegister()
{
return view('auth.register');
}
// custom method
public function getRegister()
{
if (Request::is('jobseeker/register'))
{
return view('auth.jobseeker_register');
}
elseif (Request::is('recruiter/register'))
{
return view('auth.recruiter_register');
}
}
Similarly the default postLogin method is as follows:
public function postLogin(Request $request)
{
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
$credentials = $request->only('email', 'password');
if ($this->auth->attempt($credentials, $request->has('remember')))
{
return redirect()->intended($this->redirectPath());
}
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => 'These credentials do not match our records.',
]);
}
But I would want the method to also check the user roles as follows:
public function postLogin(Request $request)
{
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
$credentials = $request->only('email', 'password');
if ($this->auth->attempt($credentials, $request->has('remember')))
{
if(Auth::user()->role->name == 'recruiter')
{
return redirect()->to('/recruiter/dashboard');
}
elseif(Auth::user()->role->name == 'jobseeker')
{
return redirect()->to('jobseeker/dashboard');
}
}
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => 'These credentials do not match our records.',
]);
}
So my question is how do you go about customizing the existing authentication? Do you guys create a new controller perhaps CustomAuthController, CustomPasswordController and copy all the traits from the default auth controllers into these custom controllers and edit them as appropriate? I'm unable to find any Laravel 5 tutorials on how to acheive this - they all simply talk about the default out of the box authentication. If anyone has done something similar before I would love to hear about how you went about it and which files were edited to wire this custom auth up.
You have a couple of options:
Override the methods in the existing auth controller.
Just don’t implement the AuthenticatesAndRegistersUsers trait at all, and implement authentication logic entirely yourself.
With regards to redirect, I’d listen on the auth.login event, check your user’s type there, and then redirect to the specific dashboard there and then.

Authentication with 2 different tables

I need to create a new "auth" config with another table and users. I have one table for the "admin" users and another table for the normal users.
But how can I create another instance of Auth with a different configuration?
While trying to solve this problem myself, I found a much simpler way. I basically created a custom ServiceProvider to replace the default Auth one, which serves as a factory class for Auth, and allows you to have multiple instances for multiple login types. I also stuck it all in a package which can be found here: https://github.com/ollieread/multiauth
It's pretty easy to use really, just replace the AuthServiceProvider in app/config/app.php with Ollieread\Multiauth\MultiauthServiceProvider, then change app/config/auth.php to look something like this:
return array(
'multi' => array(
'account' => array(
'driver' => 'eloquent',
'model' => 'Account'
),
'user' => array(
'driver' => 'database',
'table' => 'users'
)
),
'reminder' => array(
'email' => 'emails.auth.reminder',
'table' => 'password_reminders',
'expire' => 60,
),
);
Now you can just use Auth the same way as before, but with one slight difference:
Auth::account()->attempt(array(
'email' => $attributes['email'],
'password' => $attributes['password'],
));
Auth::user()->attempt(array(
'email' => $attributes['email'],
'password' => $attributes['password'],
));
Auth::account()->check();
Auth::user()->check();
It also allows you to be logged in as multiple user types simultaneously which was a requirement for a project I was working on. Hope it helps someone other than me.
UPDATE - 27/02/2014
For those of you that are just coming across this answer, I've just recently added support for reminders, which can be accessed in the same factory style way.
You can "emulate" a new Auth class.
Laravel Auth component is basically the Illuminate\Auth\Guard class, and this class have some dependencies.
So, basically you have to create a new Guard class and some facades...
<?php
use Illuminate\Auth\Guard as AuthGuard;
class CilentGuard extends AuthGuard
{
public function getName()
{
return 'login_' . md5('ClientAuth');
}
public function getRecallerName()
{
return 'remember_' . md5('ClientAuth');
}
}
... add a ServiceProvider to initialize this class, passing it's dependencies.
<?php
use Illuminate\Support\ServiceProvider;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Hashing\BcryptHasher;
use Illuminate\Auth\Reminders\PasswordBroker;
use Illuminate\Auth\Reminders\DatabaseReminderRepository;
use ClientGuard;
use ClientAuth;
class ClientServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerAuth();
$this->registerReminders();
}
protected function registerAuth()
{
$this->registerClientCrypt();
$this->registerClientProvider();
$this->registerClientGuard();
}
protected function registerClientCrypt()
{
$this->app['client.auth.crypt'] = $this->app->share(function($app)
{
return new BcryptHasher;
});
}
protected function registerClientProvider()
{
$this->app['client.auth.provider'] = $this->app->share(function($app)
{
return new EloquentUserProvider(
$app['client.auth.crypt'],
'Client'
);
});
}
protected function registerClientGuard()
{
$this->app['client.auth'] = $this->app->share(function($app)
{
$guard = new Guard(
$app['client.auth.provider'],
$app['session.store']
);
$guard->setCookieJar($app['cookie']);
return $guard;
});
}
protected function registerReminders()
{
# DatabaseReminderRepository
$this->registerReminderDatabaseRepository();
# PasswordBroker
$this->app['client.reminder'] = $this->app->share(function($app)
{
return new PasswordBroker(
$app['client.reminder.repository'],
$app['client.auth.provider'],
$app['redirect'],
$app['mailer'],
'emails.client.reminder' // email template for the reminder
);
});
}
protected function registerReminderDatabaseRepository()
{
$this->app['client.reminder.repository'] = $this->app->share(function($app)
{
$connection = $app['db']->connection();
$table = 'client_reminders';
$key = $app['config']['app.key'];
return new DatabaseReminderRepository($connection, $table, $key);
});
}
public function provides()
{
return array(
'client.auth',
'client.auth.provider',
'client.auth.crypt',
'client.reminder.repository',
'client.reminder',
);
}
}
In this Service Provider, I put some example of how to create a 'new' password reminder component to.
Now you need to create two new facades, one for authentication and one for password reminders.
<?php
use Illuminate\Support\Facades\Facade;
class ClientAuth extends Facade
{
protected static function getFacadeAccessor()
{
return 'client.auth';
}
}
and...
<?php
use Illuminate\Support\Facades\Facade;
class ClientPassword extends Facade
{
protected static function getFacadeAccessor()
{
return 'client.reminder';
}
}
Of course, for password reminders, you need to create the table in database, in order to work. In this example, the table name should be client_reminders, as you can see in the registerReminderDatabaseRepository method in the Service Provider. The table structure is the same as the original reminders table.
After that, you can use your ClientAuth the same way you use the Auth class. And the same thing for ClientPassword with the Password class.
ClientAuth::gust();
ClientAuth::attempt(array('email' => $email, 'password' => $password));
ClientPassword::remind($credentials);
Don't forget to add your service provider to the service providers list in the app/config/app.php file.
UPDATE:
If you are using Laravel 4.1, the PasswordBroker doesn't need the Redirect class anymore.
return new PasswordBroker(
$app['client.reminder.repository'],
$app['client.auth.provider'],
$app['mailer'],
'emails.client.reminder' // email template for the reminder
);
UPDATE 2
Laravel 5.2 just introduced multi auth, so this is no longer needed in this version.
Ok, I had the same problem and here is how I solved it:
actually in laravel 4 you can simply change the auth configs at runtime so to do the trick you can simply do the following in your App::before filter:
if ($request->is('admin*'))
{
Config::set('auth.model', 'Admin');
}
this will make the Auth component to use th Admin model when in admin urls. but this will lead to a new problem, because the login session key is the same if you have two users in your admins and users table with the same id you will be able to login to the admin site if you have logged in before as a regular user! so to make the two different authetications completely independent I did this trick:
class AdminGuard extends Guard
{
public function getName()
{
return 'admin_login_'.md5(get_class($this));
}
public function getRecallerName()
{
return 'admin_remember_'.md5(get_class($this));
}
}
Auth::extend('eloquent.admin', function()
{
return new AdminGuard(new EloquentUserProvider(new BcryptHasher, 'Admin'), App::make('session.store'));
});
and change the App::before code to:
if ($request->is('admin*'))
{
Config::set('auth.driver', 'eloquent.admin');
Config::set('auth.model', 'Admin');
}
you can see that I made a new auth driver and rewrote some methods on the Guard class so it will generate different session keys for admin site. then I changed the driver for the admin site. good luck.
I had the same problem yesterday, and I ended up creating a much simpler solution.
My requirements where 2 different tables in two different databases. One table was for admins, the other was for normal users. Also, each table had its own way of hashing. I ended up with the following (Code also available as a gist on Github: https://gist.github.com/Xethron/6790029)
Create a new UserProvider. I called mine MultiUserProvider.php
<?php
// app/libraries/MultiUserProvider.php
use Illuminate\Auth\UserProviderInterface,
Illuminate\Auth\UserInterface,
Illuminate\Auth\GenericUser;
class MultiUserProvider implements UserProviderInterface {
protected $providers;
public function __construct() {
// This should be moved to the config later...
// This is a list of providers that can be used, including
// their user model, hasher class, and hasher options...
$this->providers = array(
'joomla' => array(
'model' => 'JoomlaUser',
'hasher' => 'JoomlaHasher',
)
'another' => array(
'model' => 'AnotherUser',
'hasher' => 'AnotherHasher',
'options' => array(
'username' => 'empolyee_number',
'salt' => 'salt',
)
),
);
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Auth\UserInterface|null
*/
public function retrieveById($identifier)
{
// Returns the current provider from the session.
// Should throw an error if there is none...
$provider = Session::get('user.provider');
$user = $this->createModel($this->providers[$provider]['model'])->newQuery()->find($identifier);
if ($user){
$user->provider = $provider;
}
return $user;
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Auth\UserInterface|null
*/
public function retrieveByCredentials(array $credentials)
{
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
// Retrieve the provider from the $credentials array.
// Should throw an error if there is none...
$provider = $credentials['provider'];
$query = $this->createModel($this->providers[$provider]['model'])->newQuery();
foreach ($credentials as $key => $value)
{
if ( ! str_contains($key, 'password') && ! str_contains($key, 'provider'))
$query->where($key, $value);
}
$user = $query->first();
if ($user){
Session::put('user.provider', $provider);
$user->provider = $provider;
}
return $user;
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Auth\UserInterface $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserInterface $user, array $credentials)
{
$plain = $credentials['password'];
// Retrieve the provider from the $credentials array.
// Should throw an error if there is none...
$provider = $credentials['provider'];
$options = array();
if (isset($this->providers[$provider]['options'])){
foreach ($this->providers[$provider]['options'] as $key => $value) {
$options[$key] = $user->$value;
}
}
return $this->createModel($this->providers[$provider]['hasher'])
->check($plain, $user->getAuthPassword(), $options);
}
/**
* Create a new instance of a class.
*
* #param string $name Name of the class
* #return Class
*/
public function createModel($name)
{
$class = '\\'.ltrim($name, '\\');
return new $class;
}
}
Then, I told Laravel about my UserProvider by adding the following lines to the top of my app/start/global.php file.
// app/start/global.php
// Add the following few lines to your global.php file
Auth::extend('multi', function($app) {
$provider = new \MultiUserProvider();
return new \Illuminate\Auth\Guard($provider, $app['session']);
});
And then, I told Laravel to use my user provider instead of EloquentUserProvider in app/config/auth.php
'driver' => 'multi',
Now, when I authenticate, I do it like so:
Auth::attempt(array(
'email' => $email,
'password' => $password,
'provider'=>'joomla'
)
)
The class would then use the joomlaUser model, with the joomlaHasher, and no options for the hasher... If using 'another' provider, it will include options for the hasher.
This class was built for what I required but can easily be changed to suite your needs.
PS: Make sure the autoloader can find MultiUserProvider, else it won't work.
I'm using Laravel 5 native auth to handle multiple user tables...
It's not difficult, please check this Gist:
https://gist.github.com/danielcoimbra/64b779b4d9e522bc3373
UPDATE: For Laravel 5, if you need a more robust solution, try this package:
https://github.com/sboo/multiauth
Daniel