While using Sentry in L4, is it possible to make an account be used in multiple computers at the same time? Right now, Sentry logs out the user the moment the same account is used in another computer.
Right now I'm trying for that not to happen and keep both users logged in at the same time. I know that it's a security feature when a user gets logged out, but my project's circumstances aren't what you'd call normal.
Extension to Nico Kaag's answer and implementation of spamoom's comment:
/app/config/packages/cartalyst/sentry/config.php
...
// Modify users array to point to custom model.
'users' => array(
'model' => 'User',
'login_attribute' => 'email',
),
...
/app/models/User.php
use Cartalyst\Sentry\Users\Eloquent\User as SentryUser;
class User extends SentryUser
{
...
...
// Override the SentryUser getPersistCode method.
public function getPersistCode()
{
if (!$this->persist_code)
{
$this->persist_code = $this->getRandomString();
// Our code got hashed
$persistCode = $this->persist_code;
$this->save();
return $persistCode;
}
return $this->persist_code;
}
}
It is possible, but not supported by Sentry itself.
To do this, you have to change some core code in Sentry, or find a way to override the User class that's in the Sentry code.
The function you need to adjust is "GetPresistCode()" in the User model, which can be found in:
/vendor/cartalyst/sentry/src/Cartalyst/Sentry/Users/Eloquent/User.php
And this is how the function should look like (not tested):
/**
* Gets a code for when the user is
* persisted to a cookie or session which
* identifies the user.
*
* #return string
*/
public function getPersistCode()
{
if (!$this->persist_code) {
$this->persist_code = $this->getRandomString();
// Our code got hashed
$persistCode = $this->persist_code;
$this->save();
return $persistCode;
}
return $this->persist_code;
}
I have to say that I highly recommend you don't change the code in Sentry, and that you find another way around, but that might be really hard.
Related
I'am using CakePhp3 for my website and I have to inject some custom validation logic based on the current user Id when I'am creating or modifying an entity.
The basic case is "Is the user allow to change this field to this new value" ? If' not, I want to raise a validation error (or an unauthorized exception).
In cakephp, for what I'am understanding, most of the application and businness rules must be placed on Models or 'ModelsTable'of the ORM. But, in this classes, the AuthComponent or the current session is not available.
I don't want to call manually a method on the entity from the controller each time I need to check. I would like to use a validator, something like :
$validator->add('protected_data', 'valid', [
'rule' => 'canChangeProtectedData',
'message' => __('You're not able to change this data !'),
'provider' => 'table',
]);
Method on ModelTable :
public function canChangeProtectedData($value, array $context)
{
\Cake\Log\Log::debug("canChangeProtectedData");
// Find logged user, look at the new value, check if he is authorized to do that, return true/false
return false;
}
I cakephp < 3, the AuthComponent have a static method 'AuthComponent::user()' that is not available anymore. So, how Can I do that in CakePhp 3 ?
Thank you for any response.
EDIT - Adding more details
So here are more details. In case of an REST API. I have an edit function of an entity. The "Article" Entity.
This Article has an owner with a foreign key on the column named "user_id" (nothing special here). My users are organized in groups with a leader on the group. Leaders of groups can change article's owner but "basics" users can't do it (but they can edit their own articles). Admin users can edit everything.
So the edit method must be available for any authenticated user, but changing the "user_id" of the entity must be allowed and checked depending the case (if I'am admin yes, if I'am leader yes only if the new Id is one of my group and if I'am basic user no).
I can do this check on the controller but if I want this rule to be checked everywhere in my code where an Article is modified (in another method than the "Edit" of ArticlesController). So for me the Model seems the good place to put it no?
Authentication vs Authorisation
Authentication means identifying an user by credentials, which most of the time boils down to "Is a user logged in".
Authorisation means to check if an user is allowed to do a specific action
So don't mix these two.
You don't want validation you want application rules
Taken from the book:
Validation vs. Application Rules
The CakePHP ORM is unique in that it uses a two-layered approach to
validation.
The first layer is validation. Validation rules are intended to
operate in a stateless way. They are best leveraged to ensure that the
shape, data types and format of data is correct.
The second layer is application rules. Application rules are best
leveraged to check stateful properties of your entities. For example,
validation rules could ensure that an email address is valid, while an
application rule could ensure that the email address is unique.
What you want to implement is complex application logic and more than just a simple validation, so the best way to implement this is as an application rule.
I'm taking a code snippet from one of my articles that explains a similar case. I had to check for a limitation of languages (translations) that can be associated to a model. You can read the whole article here http://florian-kraemer.net/2016/08/complex-application-rules-in-cakephp3/
<?php
namespace App\Model\Rule;
use Cake\Datasource\EntityInterface;
use Cake\ORM\TableRegistry;
use RuntimeException;
class ProfileLanguageLimitRule {
/**
* Performs the check
*
* #link http://php.net/manual/en/language.oop5.magic.php
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #param array $options Options.
* #return bool
*/
public function __invoke(EntityInterface $entity, array $options) {
if (!isset($entity->profile_constraint->amount_of_languages)) {
if (!isset($entity->profile_constraint_id)) {
throw new RuntimeException('Profile Constraint ID is missing!');
}
$languageLimit = $this->_getConstraintFromDB($entity);
} else {
$languageLimit = $entity->profile_constraint->amount_of_languages;
}
// Unlimited languages are represented by -1
if ($languageLimit === -1) {
return true;
}
// -1 Here because the language_id of the profiles table already counts as one language
// So it's always -1 of the constraint value
$count = count($entity->languages);
return $count <= ($languageLimit - 1);
}
/**
* Gets the limitation from the ProfileConstraints Table object.
*
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #return int
*/
protected function _getConstraintFromDB(EntityInterface $entity) {
$constraintsTable = TableRegistry::get('ProfileConstraints');
$constraint = $constraintsTable->find()
->where([
'id' => $entity['profile_constraint_id']
])
->select([
'amount_of_languages'
])
->firstOrFail();
return $constraint->amount_of_languages;
}
}
I think it is pretty self-explaining. Make sure your entities user_id field is not accessible for the "public". Before saving the data, just after the patching add it:
$entity->set('user_id', $this->Auth->user('id'));
If you alter the above snippet and change the profile_constraint_id to user_id or whatever else you have there this should do the job for you.
What you really want is row / field level based authorisation
Guess you can use ACL for that, but I've never ever had the need for field based ACL yet. So I can't give you much input on that, but it was (Cake2) and still is (Cake3) possible. For Cake3 the ACL stuff was moved to a plugin. Technically it is possible to check against anything, DB fields, rows, anything.
You could write a behavior that uses the Model.beforeMarshal event and checks if user_id (or role, or whatever) is present and not empty and then run a check on all fields you want for the given user id or user role using ACL.
You could probably use this method PermissionsTable::check() or you can write a more dedicated method does checks on multiple objects (fields) at the same time. Like I said, you'll spend some time to figure the best way out using ACL if you go for it.
UX and yet another cheap solution
First I would not show fields at all an user is not allowed to change or enter as inputs. If you need to show them, fine, disable the form input or just show it as text. Then use a regular set of validation rules that requires the field to be empty (or not present) or empty a list of fields based on your users role. If you don't show the fields the user would have to temper the form and then fail the CSRF check as well (if used).
I don't think you need to validate in the table. I just thought of a way to do it in the controller.
In my Users/Add method in the controller for instance:
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->data);
//check if user is logged in and is a certain user
if ($this->request->session()->read('Auth.User.id') === 1) {
//allow adding/editing role or whatever
$user->role = $this->request->data('role');
} else {
$user->role = 4;//or whatever the correct data is for your problem.
}
if ($this->Users->save($user)) {
$this->Flash->success(__('You have been added.'));
} else {
$this->Flash->error(__('You could not be added. Please, try again.'));
}
}
$this->set(compact('user'));
$this->set('_serialize', ['user']);
}
I am currently developping an application using Symfony2.6 And I have some trouble. I have two kind of users customers and employee so in my Database I have a table customer and a table employee. How can I tell symfony to go look in these two tables for the user credentials when the user try to log in?
I know that you can define a provider in the security.yml file which can be the entity but how can I declare two providers? is that possible or do I have to do this another way ?
thank all for you answers
I've got something similar with (currently) two different types of users. To handle all of the login and permissions though, I'm using FosUserBundle, and assigning them roles which I can check and then fetch user-profiles based on that.
To ensure that they get the correct ROLE_* assigned on the user creation I've followed the SO question, Adding new FOSUserBundle users to a default group on creation. They pickup a session variable with the future-role they will take, and then I have an event listener to set that role to the newly minted FosUser:
<?php
class UserCreationListener implements EventSubscriberInterface
{
// lots of constructor setup, debugging, logging and checks removed from code
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$user = $event->getForm()->getData();
$newRole = $this->session->get('futureRole', 'ROLE_CUSTOMER');
$user->addRole($role);
$this->userManager->updateUser($user);
return $this->redirect->redirect('app_profile', 302);
}
}
So thx to someone here i've found the answer to this specific problem here
http://symfony.com/doc/current/cookbook/security/multiple_user_providers.html
Symfony allows us to declare a multiple user providers
How can I get the name of the logged-in-user in yii2?
I can get the user-id with
Yii::$app->user->id;
and I know that I could find the name in the database but I want a direct way.
The name-column in the database has the name "username", but
Yii::$app->user->username;
doesn't work and
Yii::$app->user->name;
doesn't work either.
On login the user information will be stored in Yii::$app->user->identity variable.
For more information have a read through the User Authentication documentation in the official guide.
While the answer from #thepeach works, you can actually extend the User component and add your own functions, so that you can get them via Yii::$app->user->something as you were initially trying to do.
I like to extend things like this from the start, so I am ready to add custom functionality without having to refactor any code. It sucks to do things one way, then have to go back and fix 100 spots of code, because you changed it later.
First, define a user component class in your config:
'components' => [
'user' => [
'class' => 'app\components\User', // extend User component
],
],
Then create User.php in your components directory. If you haven't made this directory, create it in your app root.
User.php
<?php
namespace app\components;
use Yii;
/**
* Extended yii\web\User
*
* This allows us to do "Yii::$app->user->something" by adding getters
* like "public function getSomething()"
*
* So we can use variables and functions directly in `Yii::$app->user`
*/
class User extends \yii\web\User
{
public function getUsername()
{
return \Yii::$app->user->identity->username;
}
public function getName()
{
return \Yii::$app->user->identity->name;
}
}
Now you can access these through Yii::$app->user->something.
For example, put this in one of your views and access the page in your browser:
<?= \Yii::$app->user->username ?>
I wrote a more detailed answer here, which covers this a bit more in depth.
Easy, just use:
<?= \Yii::$app->user->identity->username ?>
$user_id = Yii::$app->user->id;
$user_name = User::find()->where(['id'=>$user_id])->one()->username;
I always prefer below code to find current user. use only this and you will get the identity.
\Yii::$app->user->identity->username;
Is there a way to check if a user already has a valid session on a different machine?
What I want to do is when a user logs in, destroy an other sessions which they may already have, so that if they forget to logout from a computer say on campus or at work, and then they log in at home, it will destroy those other 2 sessions so they are no longer logged in?
Facebook employs this in some way.
My only thoughts so far is something to this effect:
$user = User::find(1); // find the user
Auth::login($user); // log them in
Auth::logout(); // log them out hoping that it will destroy all their sessions on all machines
Auth::login($user); // log them in again so they have a valid session on this machine
I have not had the chance to test this, and I do not know if Auth::login($user); will destroy all sessions for that user, or only the current one.
Thanks!
You can save a session_id within a user model, so that:
When logout event is fired (auth.logout) you would clear it.
When new logging event is fired you can check if attribute session_id is not null within the user model.
If it's not - destroy previous session by:
Session::getHandler()->destroy($user->session_id);
$user->session_id = Session::getId();
Hope that would help!
I realise this is an old question, but there is now a method in laravel 5.6 that does exactly this, so it may be useful for someone coming to this later. You can also retro-fit this method to earlier versions of laravel very easily.
See the docs at https://laravel.com/docs/5.6/authentication#invalidating-sessions-on-other-devices
I had the same use case as you (log out all other devices on log-in). I overrode the default login method to add my own custom logic (first copying the default login method from vendor/laravel/framework/src/illuminate/Foundation/Auth/AuthenticatesUsers.php)
In that method, there is the line if ($this->attemptLogin($request)) - within this, before the return statement, add your call to logoutOtherDevices, as below
if ($this->attemptLogin($request)) {
//log out all other sessions
Auth::logoutOtherDevices($request->password); //add this line
return $this->sendLoginResponse($request);
}
Also ensure you have un-commented the Illuminate\Session\Middleware\AuthenticateSession middleware in your app/Http/Kernel.php, as per the docs
(note that I haven't tested the above code as I was using an older version of laravel that doesn't have this method, see below). This should work in 5.6 though.
Older Laravel versions
I was actually using laravel 5.5, so didn't have access to this handy method. Luckily, it's easy to add.
I opened a laravel 5.6 project and copied the logoutOtherDevices method from vendor/laravel/framework/src/illuminate/Auth/SessionGuard.php - for reference I have pasted below
/**
* Invalidate other sessions for the current user.
*
* The application must be using the AuthenticateSession middleware.
*
* #param string $password
* #param string $attribute
* #return null|bool
*/
public function logoutOtherDevices($password, $attribute = 'password')
{
if (! $this->user()) {
return;
}
return tap($this->user()->forceFill([
$attribute => Hash::make($password),
]))->save();
}
I then copied this into my LoginController - it could go somewhere else of your choice, but I've put it here for ease / laziness. I had to modify it slightly, as below ($this->user() becomes Auth::user())
/**
* Invalidate other sessions for the current user.
* Method from laravel 5.6 copied to here
*
* The application must be using the AuthenticateSession middleware.
*
* #param string $password
* #param string $attribute
* #return null|bool
*/
public function logoutOtherDevices($password, $attribute = 'password')
{
if (! Auth::user()) {
return;
}
return tap(Auth::user()->forceFill([
$attribute => Hash::make($password),
]))->save();
}
I can then call this method in my login method, as specified earlier in my answer, with a slight adjustment - $this->logoutOtherDevices($request->password);
If you want to test this locally, it seems to work if you open your site on a normal and an incognito window. When you log in on one, you'll be logged out on the other - though you'll have to refresh to see anything change.
I hope you will see this job:
Session::regenerate(true);
a new session_id be obtained.
This may not be the best answer, but first thing that came to my mind was lowering the timeout on the session.
In app->config->session.php there's a setting for both lifetime and expire_on_close (browser).
I'd try looking into that for now, and see if someone else comes up with something better.
I saw some codes on internet which in order to check the permissions to access a concrete action, they use the Configure::read function in this way:
public function action1(){
if(!Configure::read('isAdmin')){
$this->redirect(array('controller' => 'depots', 'action' => 'status'));
}
//whatever
}
I was wondering, which is the difference between using Configure::read and Configure:write for this purpose and using $this->Session->read() and $this->Session->write()?
Which is a better way to check it?
Thanks.
Using the AuthComponent
If you make use of the built-in AuthComponent, CakePHP will store details of the currently logged-in user inside the session.
Getting properties of the currently logged-in User
Once logged in, you can access the information of the Used (e.g. role_id) via the AuthComponent. This can be done anywhere (also inside your Views or Models if desired);
For example;
if (123 === AuthComponent::user('role_id')) {
debug('hello admin user');
}
Or, inside a Controller:
if (123 === $this->Auth->user('role_id')) {
debug('hello admin user');
}
Accessing the logged in user
However, to dont have to repeat the group-id everywhere, it's best to creat a method for this (e.g inside your AppController);
/**
* Checks if the currently logged in user is an admin
*
* #return bool true if the current user is an admin
*/
protected function isAdmin()
{
// probably best to make the id configurable (Configure::write())?
return (123 === $this->Auth->user('role_id'));
}
Access control
To use a 'simple' authorisation, you can create your own isAuthorized() action in your Controller, which will allow you to block access to specific actions, based on the properties of the currently logged-in user;
Using ControllerAuthorize
I can't see why you would put the user role in the Configure array, as it is intended to contain application wide settings.
Personaly I have a table in my database that contain the roles. Although some roles may be added to it, there are some that I never modify (typically the administrator role).
This allows me to store its value as an application parameter in Configure and check for it later:
bootstrap.php
Configure :: write('administrator.role_id', 1);
TestController:
if($this->Auth->user('role_id') == Configure :: read('administrator.role_id'))
{
//do things specific to admin role
}
That said if the user role is stored dynamically in one way or another in Configure, it could probably work as well, but that's probably not the more elegant solution.