When are user roles refreshed and how to force it? - security

First off, I'm not using FOSUserBundle and I can't because I'm porting a legacy system which has its own Model layer (no Doctrine/Mongo/whatsoever here) and other very custom behavior.
I'm trying to connect my legacy role system with Symfony's so I can use native symfony security in controllers and views.
My first attempt was to load and return all of the user's roles in the getRoles() method from the Symfony\Component\Security\Core\User\UserInterface. At first, it looked like that worked. But after taking a deeper look, I noticed that these roles are only refreshed when the user logs in. This means that if I grant or revoke roles from a user, he will have to log out and back in for the changes to take effect. However, if I revoke security roles from a user, I want that to be applied immediately, so that behavior isn't acceptable to me.
What I want Symfony to do is to reload a user's roles on every request to make sure they're up-to-date. I have implemented a custom user provider and its refreshUser(UserInterface $user) method is being called on every request but the roles somehow aren't being refreshed.
The code to load / refresh the user in my UserProvider looks something like this:
public function loadUserByUsername($username) {
$user = UserModel::loadByUsername($username); // Loads a fresh user object including roles!
if (!$user) {
throw new UsernameNotFoundException("User not found");
}
return $user;
}
(refreshUser looks similar)
Is there a way to make Symfony refresh user roles on each request?

So after a couple of days trying to find a viable solution and contributing to the Symfony2 user mailing list, I finally found it. The following has been derived from the discussion at https://groups.google.com/d/topic/symfony2/NDBb4JN3mNc/discussion
It turns out that there's an interface Symfony\Component\Security\Core\User\EquatableInterface that is not intended for comparing object identity but precisely to
test if two objects are equal in security and re-authentication context
Implement that interface in your user class (the one already implementing UserInterface). Implement the only required method isEqualTo(UserInterface $user) so that it returns false if the current user's roles differ from those of the passed user.
Note: The User object is serialized in the session. Because of the way serialization works, make sure to store the roles in a field of your user object, and do not retrieve them directly in the getRoles() Method, otherwise all of that won't work!
Here's an example of how the specific methods might look like:
protected $roles = null;
public function getRoles() {
if ($this->roles == null) {
$this->roles = ...; // Retrieve the fresh list of roles
// from wherever they are stored here
}
return $this->roles;
}
public function isEqualTo(UserInterface $user) {
if ($user instanceof YourUserClass) {
// Check that the roles are the same, in any order
$isEqual = count($this->getRoles()) == count($user->getRoles());
if ($isEqual) {
foreach($this->getRoles() as $role) {
$isEqual = $isEqual && in_array($role, $user->getRoles());
}
}
return $isEqual;
}
return false;
}
Also, note that when the roles actually change and you reload the page, the profiler toolbar might tell you that your user is not authenticated. Plus, looking into the profiler, you might find that the roles didn't actually get refreshed.
I found out that the role refreshing actually does work. It's just that if no authorization constraints are hit (no #Secure annotations, no required roles in the firewall etc.), the refreshing is not actually done and the user is kept in the "unauthenticated" state.
As soon as you hit a page that performs any kind of authorization check, the user roles are being refreshed and the profiler toolbar displays the user with a green dot and "Authenticated: yes" again.
That's an acceptable behavior for me - hope it was helpful :)

In your security.yml (or the alternatives):
security:
always_authenticate_before_granting: true
Easiest game of my life.

From a Controller, after adding roles to a user, and saving to the database, simply call:
// Force refresh of user roles
$token = $this->get('security.context')->getToken()->setAuthenticated(false);

Take a look here, set always_authenticate_before_granting to true at security.yml.

I achieve this behaviour by implementing my own EntityUserProvider and overriding loadByUsername($username) method :
/**
* Load an user from its username
* #param string $username
* #return UserInterface
*/
public function loadUserByUsername($username)
{
$user = $this->repository->findOneByEmailJoinedToCustomerAccount($username);
if (null === $user)
{
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
//Custom function to definassigned roles to an user
$roles = $this->loadRolesForUser($user);
//Set roles to the user entity
$user->setRoles($roles);
return $user;
}
The trick is to call setRoles each time you call loadByUsername ... Hope it helps

Solution is to hang a subscriber on a Doctrine postUpdate event. If updated entity is User, same user as logged, then I do authenticate using AuthenticationManager service. You have to inject service container (or related services) to subscriber, of course. I prefer to inject whole container to prevent a circular references issue.
public function postUpdate(LifecycleEventArgs $ev) {
$entity = $ev->getEntity();
if ($entity instanceof User) {
$sc = $this->container->get('security.context');
$user = $sc->getToken()->getUser();
if ($user === $entity) {
$token = $this->container->get('security.authentication.manager')->authenticate($sc->getToken());
if ($token instanceof TokenInterface) {
$sc->setToken($token);
}
}
}
}

Sorry i cant reply in comment so i replay to question. If someone new in symfony security try to get role refresh work in Custom Password Authentication then inside function authenticateToken :
if(count($token->getRoles()) > 0 ){
if ($token->getUser() == $user ){
$passwordValid=true;
}
}
And do not check for passwords from DB/LDAP or anywhere. If user come in system then in $token are just username and had no roles.

I've been battling this for Symfony4, and I think I've finally settled down to a solution.
The thing is that in my case, the roles depend on the "company" the user is working with. It may be a CEO in one company, but an operator in another one, and the menus, permissions, etc. depend on the company. When switching companies, the user must not re-login.
Finally I've done the following:
Set the firewall to stateless.
In the FormAuthentication class, I set an attribute in the session explicitely, with the username.
I set up another Guard, which essentially take this attribute and loads the user for it from the database, for every single request.
class FormAuthenticator extends AbstractFormLoginAuthenticator
{
/** Constructor omitted */
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'nomusuari' => $request->request->get('nomusuari'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['nomusuari']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $userProvider->loadUserByUsername($credentials['nomusuari']);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Invalid user/password');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
$valid = $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
return $valid;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$request->getSession()->set("user_username",$token->getUsername());
return new RedirectResponse(
$this->urlGenerator->generate("main")
);
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
The SessionAuthenticator (returns JSON, you may have to adapt it):
class SessionAuthenticator extends AbstractGuardAuthenticator
{
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request)
{
return $request->getSession()->has("user_username");
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser() as $credentials.
*/
public function getCredentials(Request $request)
{
return $request->getSession()->get("user_username","");
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if (null === $credentials) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
return null;
}
// if a User is returned, checkCredentials() is called
/*return $this->em->getRepository(User::class)
->findOneBy(['apiToken' => $credentials])
;*/
return $userProvider->loadUserByUsername($credentials);
}
public function checkCredentials($credentials, UserInterface $user)
{
// Check credentials - e.g. make sure the password is valid.
// In case of an API token, no credential check is needed.
// Return `true` to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
// you may want to customize or obfuscate the message first
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Finally, my security.yaml:
main:
anonymous:
stateless: true
guard:
entry_point: App\Security\FormAuthenticator
authenticators:
- App\Security\SessionAuthenticator
- App\Security\FormAuthenticator
Working fine. I can see the changes in the toolbar, and the Roles are refreshed.
HTH,
Esteve

Related

Validate date from other bounded context

I have two Bounded Context (studentenrollment, courses).
Studentenrollment has all the student with his course ids and his homework.
Courses have the admin part that content all the information related with the course.
When a student want to get information of the course, it hits an endpoint( /courses/ID) sending the jwt token. In the course context I get the student ID, course ID and create query that it's dispatched in the bus. In the query handler before getting the information of the course from the course ID, I want to validate if the student ID exist and this student has this course. For that I have to call the another context bounded studentenrollment. So, I was looking for how to handle that on internet and I found this:
https://medium.com/#martinezdelariva/authentication-and-authorization-in-ddd-671f7a5596ac
class findByCourseIdAndStudentIdQueryHandler()
{
public function handle($findByCourseIdAndStudentIdQuery)
{
$courseId = $findByCourseIdAndStudentIdQuery->courseId();
$studentId = $findByCourseIdAndStudentIdQuery->studentId();
$student = $this->collaboratorService->studentFrom(
$courseId,
$studentId
);
$this->courseRepository->findByCourseId($courseId);
}
}
class collaboratorService()
{
public function studentFrom($courseId, $studentId)
{
$student = $this->studentEnrollmentClient->getStudentFrom($courseId, $studentId);
if (!$student) {
throw new InvalidStudentException();
}
return $student;
}
}
What do you think?
UPDATED
namespace App\Context\Course\Module\Course\UI\Controller;
class GetCourseController extends Controller
{
public function getAction($request) {
$this->ask(new FindByCourseIdQueryHandler($request->get('course_id'));
}
}
namespace App\Context\Course\Module\Course\Infrastracture\Query;
class AuthorizedQueryDispatcher extends QueryDispatcher
{
//In this case $query would be FindByCourseIdQueryHandler
public function handle($query)
{
$authUser = $this->oauthService->getAuthUser();
//it can be student or teacher
$role = $authUser->getRole();
$userId = $authUser->getUserId();
//it will return FindByCourseIdAndStudentIdAuthorizedQueryHandler
$authorizedQuery = $this->inflector->getAuthorizedQueryName->from($userId, $role, $query);
$this->dispatch($authorizedQuery);
$this->queryDispatch->dispatch($query);
}
}
namespace App\Context\Course\Module\Course\Application\Query;
class FindByCourseIdAndStudentIdAuthorizedQueryHandler
{
public function handle($findByCourseIdAndStudentIdQuery)
{
$student = $this->studentEnrollmentClient->getStudentFrom($findByCourseIdAndStudentIdQuery->courseId, $findByCourseIdAndStudentIdQuery->studentId);
if (!$student) {
throw new InvalidStudentException();
}
}
}
namespace App\Context\Course\Module\Course\Application\Query;
class findByCourseIdAndStudentIdQueryHandler()
{
public function handle($findByCourseIdQueryHandler)
{
$courseId = $findByCourseIdQueryHandler->courseId();
$this->courseRepository->findByCourseId($courseId);
}
}
TLDR; Authorization should be clearly separated from the Domain layer, for example in a different package/namespace/module. Also, the dependency from the Domain to the Authorization should be inverted, the Domain should not depend/know about the authorization/
One way to implement it is to create an Authorization service, for example FindByCourseIdAndStudentIdQueryAuthorizer (let's name it Authorizer). This service may cross Bounded context (BC) boundaries, i.e. it could depend on remote domain services from remote BCs. Ideally, the remote data should be already available when the Authorizer does the checking. In this way the system is more resilient in case remote Bounded context services are not available. You can do this by listening to remote events or by background tasks.
Ideally, the domain layer (from any BC) should not know about the Authorizers.
One way to do this is to decorate the QueryDispatcher (or what you have) in the Composition root of the application with an AuthorizedQueryDispatcher. This AuthorizedQueryDispatcher, when it receives a query, it first search an Authorizer and then calls it. If the authorization fails then the query is rejected. If the authorization succedds or there is not authorizer then the query is sent to the real/decorated QueryDispatcher.
If can't do this (i.e. you don't have a QueryDispatcher) then you can try to decorate every query handler (by hand?). For example, you could have a FindByCourseIdAndStudentIdAuthorizedQueryHandler that has the same interface as the FindByCourseIdAndStudentIdQueryHandler. You could replace them in the composition root of the application (DIC).

Yii file permission verify doesn't work

I have a question about yii.
I have some videos and the view action is performed based on some privileges. Some videos may be hidden, just for some user categories or for all. I can handle this from yii filter or something, but the real problem is what happen if someone knows the video file path, i can't handle the permissions in this case.
My solution is ok, but I have one problem - when someone access direct a file I redirect it (from htaccess) to a page, when I verify the user permission based on it's id. When I try to access the file from url everything is ok, the permission filter works fine, but when I the file is called from the player something it's wrong. From what I saw, the Yii::app()->user->id is empty.
Any recommendation?
This is my UserIdentity class:
class UserIdentity extends CUserIdentity
{
private $_id;
/**
* Authenticates a user.
* #return boolean whether authentication succeeds.
*/
public function authenticate()
{
$user=Utilizator::model()->findByAttributes(array('username'=>$this->username));
if($user===null)
{
$this->errorCode=self::ERROR_USERNAME_INVALID;
}
else
{
if($user->password!==$user->encrypt($this->password))
{
$this->errorCode=self::ERROR_PASSWORD_INVALID;
}
else
{
$this->_id = $user->id;
$this->errorCode=self::ERROR_NONE;
}
}
return !$this->errorCode;
}
public function getId()
{
return $this->username;
}
}

How to allow action using security component?

I am getting this error
Call to undefined method SecurityComponent::allowedActions()
When I try to allow singup action in controller like this
public function beforeFilter() {
parent::beforeFilter();
$this->Security->allowedActions(array('sign-up'));
$this->Auth->allow('login','signup','index','activate','logout','forgot','reset','display');
if($this->Auth->user('id')) {
$this->set('logged_in', true);
} else {
$this->set('logged_in', false);
}
}
public $components = array('RequestHandler');
if i remove
$this->Security->allowedActions(array('sign-up'));
when I submit signup form, It shows your request has ben blackholed
There is no such method, allowedActions is a property of the SecurityComponent.
http://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::$allowedActions
$this->Security->allowedActions = array('sign-up');
Also you are using signup in AuthComponent::allow(), so make sure sign-up is really the correct name of the action (which I really doubt as this would be invalid PHP syntax).

Symfony 2 ACL vs Voters

I want to use isGranted('EDIT', $userObject) for allow edit given user data by all administrators and managers and that one user.
Should I use ACL for control edit $userObject?
I have written extra Voter which check if logged user and given object are the same or user is manager or admin.
In acl I must add ACE for userObject for all administrators, managers and that one user.
Wchich way is recommended?
I am new in Symfony..
below is voter's code:
function vote(TokenInterface $token, $object, array $attributes)
{
$intersect=array_intersect(array('EDIT','VIEW' ), $attributes);
if (!empty($intersect))
{
//intersect is not empty, it seems to edit or view are in $attributes
//voter grants privileges for [user->granted object]
//manager->every customer, child-manager
//admin->every customer and manager
if ($token->getUser()->isAdmin())
{
return VoterInterface::ACCESS_GRANTED;
}
elseif ($token->getUser()->isCustomer())
{
//voter not want to think about customer grants, because customer grants currently are held in ACL
return VoterInterface::ACCESS_ABSTAIN;
}
/* #var $object \PSB\StoreBundle\Entity\Customer */
if (is_a($object, '\PSB\StoreBundle\Entity\Customer'))
{
if ($token->getUser()->isManager())
{
//managers also edit customers
return VoterInterface::ACCESS_GRANTED;
}
}
elseif (is_a($object, '\PSB\StoreBundle\Entity\Manager'))
{
/* #var $object \PSB\StoreBundle\Entity\Manager */
if ($token->getUser()->isManager())
{
//manager can edit own children
if ($token->getUser() == $object->getParent())
{
return VoterInterface::ACCESS_GRANTED;
}
}
}
}
return VoterInterface::ACCESS_ABSTAIN;
}
When your model already stores the data required to know if an action should be granted or not, it's really annoying to keep the ACL in sync with your real data.
So you should obviously implement your own voters for this.
PS: You should use $object instanceof Class instead of is_a($object, 'Class')

Don't save URL in history, any header or meta-tag?

Is there any HTTP-headers or meta-tags one can use to avoid getting a URL into the browser history?
For example, I don't want
http://domain.td/show/super-secret-unique-token-that-is-private
to show up in the browser URL bar, when I start typing "domain.t".
Currently I have a (POST) search form on the website to load the tokens, and they don't come up. But later I want to load the tokens via links, from let's say an album.
I don't think you can.
You can save the token as a cookie, or use it as a GET param but make it expire every 15 minutes or so (and regenerate a new one on every page load). Also check for the same user agent, and if you want to go down the IP road, IP address (however it can give false positives, I wouldn't recommend it).
Decided to use a map that I save in the browser session. This way i can pass the tokenKey throgh the URL and get the variable back afterwards.
I wrote this little extended class of Zend_Session_Namespace and added 'add' and 'get' functions.
<?php
class My_Session_Tokens extends Zend_Session_Namespace {
protected $_namespace = "Tokens";
public function __construct($namespace = 'Tokens', $singleInstance = false)
{
parent::__construct($namespace, $singleInstance);
}
public function add($token) {
if($tokenKey = $this->hasToken($token)) {
return $tokenKey;
}
do { $tokenKey = uniqid(); } while(isset($this->$tokenKey));
$this->$tokenKey = $token;
return $tokenKey;
}
public function get($tokenKey) {
if(isset($tokenKey)) {
return $this->$tokenKey;
}
return null;
}
public function hasToken($token) {
foreach($this as $key => $val) {
if($val === $token) return $key;
}
return false;
}
}

Resources