I need to check if the user has accepted the latest privacy policy, before executing any controller. Something like this:
if($user->getAcceptedTnc() < 2) // unless I'm in some specific routes...
{
return $this->render('app/privacyPolicy.html.twig');
// or alternatively do AppController::privacyPolicyAction()
}
Where can this be done?
I've thought at logging out all the users and putting this in some authentication listener...
I solved with a onKernelController listener, so I'm doing the check at every page load:
public function onKernelController(FilterControllerEvent $event)
{
// [ return on some conditions (ajax calls, specific controller/routes, ...) ]
/** #var User $user */
$user = $this->token ? $this->token->getUser() : null;
if($user && $user !== 'anon.' && $user->getLastAcceptedTerms() < Utils::CURRENT_TERMS_VERSION) {
// [ return if the user is non-EU, has specific roles, etc...]
// The user must accept the new Terms. Show him AppController:privacyPolicyAction
$request = new Request();
$request->attributes->set('_controller', 'App\Controller\AppController:privacyPolicyAction');
$event->setController($this->controllerResolver->getController($request));
}
}
Related
I have pages like 'localhost/articles/show/id', representing the article details with the corresponding id.
I'd like to restrict the page access to a group of people.
In my database, each User belongs to a Family and each Article belongs to a Family as well.
And I want the users to be able to access article informations only if the article has been created by the family that the user is member of.
I could just verify manually by comparing the article's family to the current user family with some request in the Controller before rendering but I would to duplicated this code for every page like '/show/id', '/edit/id', ... Yet I'd like to know if there is a more beautiful way of doing it with symfony, something like 'every page that refers to a specific Article (/edit/id, /show/id and so on so forth) use a specific class to verify if the user is a member of the Family that created the article.
I think the thing you're looking for is Voter.
Security voters are the most granular way of checking permissions. All voters are
called each time you use the isGranted() method on Symfony’s authorization
checker or call denyAccessUnlessGranted() in a controller.
see: https://symfony.com/doc/current/security/voters.html
// src/Security/PostVoter.php
//....
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ArticleVoter extends Voter
{
// these strings are just invented: you can use anything
const VIEW = 'view';
const EDIT = 'edit';
/**
* return true if the voter support your entity ($subject) type
*/
protected function supports(string $attribute, $subject)
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
// only vote on `Article` objects
if (!$subject instanceof Article) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// you know $subject is a Article object, thanks to `supports()`
/** #var Article $post */
$article = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($article, $user);
case self::EDIT:
return $this->canEdit($article, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canView(Article $article, User $user)
{
//Return true if user can view article, false otherwise
}
private function canEdit(Article $article, User $user)
//Return true if user can edit article, false otherwise
}
}
Voters are used when you call $this->denyAccessUnlessGranted(String $actionName, $entity) from your controllers. this method will throws an exception if a voter that support your $entity type and your $actionName return false.
// src/Controller/ArticleController.php
// ...
class ArticleController extends AbstractController
{
/**
* #Route("/article/{id}", name="article_show")
*/
public function show($id)
{
$article = ...;
// check for "view" access: calls all voters
$this->denyAccessUnlessGranted('view', $article);
// ...do your stuff
}
/**
* #Route("/article/{id}/edit", name="article_edit")
*/
public function edit($id)
{
$article = ...;
// check for "edit" access: calls all voters
$this->denyAccessUnlessGranted('edit', $article);
// ... do your stuff
}
}
I want a login with a custom field to authenticate users into the platform.
The point is to check a field 'pw_expires_at' to \DateTime('now'), to log the user.
Here's what I did so far:
In the controller:
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$mdp)
);
$user->setPwExpiresAt(new \DateTime("now + 1 minute"));
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
In the Authenticator:
public function checkCredentials($credentials, UserInterface $user)
{
$valid = false;
$validDate = $this->checkDate($credentials, $user);
$validPassword = $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
if($validDate && $validPassword) {
$valid = true;
}
return $valid;
}
/**
* #return bool
*/
public function checkDate($credentials, UserInterface $user){
$now = new \DateTime('now');
$pwdate = new \DateTime();
$pwdate = $this->entityManager->getRepository(Users::class)->findOneBy([
'email' => $credentials['email']
]);
if ($pwdate > $now) {
return false;
}
else {
return true;
}
}
I also added the new function checkDate() in the AuthenticatorInterface.php.
The problem is : I can log in at anytime.
You are comparing (>) a user object repository->findBy(...) which returns a Users::class with a DateTime object $now = new \DateTime();.
Also the $user object entityManager reponse is most likely the same object returned by your getUsername function (the one you pass as an argument in this function) and thus can be skipped? If it is a DTO that does not contain this expired value then add it back in.
Also you are not using the credentials for anything anymore then so removed it as well.
I would change this to something like:
public function checkDate(UserInterface $user) {
$now = new \DateTime();
$pwdate = $user->getPwExpiresAt();
// we dont need the if/else as this ($pwdate > $now)
// is an expression and will already return true/false;
return $pwdate > $now;
}
Some more suggestions:
You might want to reconsider renaming the function to something more expressive like $this->hasAuthenticationExpired($user) this should give a clear indication of what the function is doing other than "checking a date (for what?!)" without reading through the function.
You can move this function to the user object like
public function hasExpired() { return $this->getPwExpiresAt() && new \DateTime() > $this->getPwExpiresAt(); }
and just call if (!$user->hasExpired()) { which is actually a preferred way for many people as this can be easily reused and accessed whenever handling the user object anywhere.
I'm running Symfony 3.4.14 and I developed my own User Bundle, I have a very bad experience with FOS then I don't want to use anymore. My goal is to let the admin users create/edit/remove users, I mean other users account.
I made :
the User entity
the Login form
the Registration form
.. and I'm stuck with the Update form. I want to let the admins edit a user without editing the password, but to give them the opportunity to do it if needed. Below is my EditUserAction in controller :
<?php
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function EditUserAction($id, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$user = $this->getDoctrine()->getRepository('CoreBundle:User')->findOneBy([ 'id'=>$id, 'deleted' => 0 ]);
if ( $user )
{
$old_password = $user->getPassword();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
// If admin changed the user password
if ( $user->getPlainPassword() )
{
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
}
// If admin didn't change the user password, we persist the old one
else
{
$user->setPassword($old_password);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
}
return array('form' => $form->createView());
}
return $this->redirectToRoute('admin_users');
}
Case 1 (when admin choose to change the user password) works well, but the other case (when admin don't want to change the password) fails. They are no way to let the 2 password inputs empty. I can't get rid of this validation error in the debug toolbar :
Path: data.plainPassword
Message: This value should not be blank
In order to avoid this error, as you can see in my controller above, I try to keep the old one (may not be a best practice, I know).
In your form / entity (where you defined validations) you should write either your own constraint (documentation) or validation callback (documentation). In there you can check - If value is null, don't validate, if not null run your validations.
You need to have two separate methods for both.
public function changePasswordAction(Request $request)
{
// your code for changing password.
}
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function editUserAction(User $user = null, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
//you can directly give your User Entity reference in parameters and you dont need to write an extra query to find user.
if ( $user === null){
//return user not found
}
else if($user->isDeleted() === false)
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
//your code
}
// your code
}
return $this->redirectToRoute('admin_users');
}
My website is running Symfony 3.4 and I made my own user member system.
My User entity contains a Datetime field 'lastLogin' and I can't find a solution to update it every time a user logged in.
I created a custom UserChecker then I tried to update the field in it :
<?php
namespace CoreBundle\Security;
use CoreBundle\Entity\User as AppUser;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() )
{
throw new AuthenticationException();
}
else
{
// BELOW IS WHAT I TRY, BUT FAIL.
$entityManager = $this->get('doctrine')->getManager();
$user->setLastLogin(new \DateTime());
$entityManager->persist($user);
$entityManager->flush();
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
}
}
But it doesn't work. Maybe I can't use the doctrine entity manager in this file ?
If I use $this->get('doctrine')->getManager(); I get :
Fatal Error: Call to undefined method
CoreBundle\Security\UserChecker::get()
Dunno why #doncallisto removed his post. It was (IMHO) the right thing.
Take a look at http://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events
So you have several options.
SecurityEvents::INTERACTIVE_LOGIN - triggers every time the user
full out the login form and submit credentials. Will work, but you
won't get last_login updates if you have remember_me cookie or similar
AuthenticationEvents::AUTHENTICATION_SUCCESS - triggers each time
(every request) when authentication was successful. It means your last_login will be updated each time on every request unless user logged out
so you'll need a EventSubscriber. Take a look at this article. https://thisdata.com/blog/subscribing-to-symfonys-security-events/
MAybe you'll need a simplified version.
public static function getSubscribedEvents()
{
return array(
// AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', // no need for this at that moment
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin', // this ist what you want
);
}
and then the onSecurityInteractiveLogin method itself.
public function onSecurityInteractiveLogin( InteractiveLoginEvent $event )
{
$user = $this->tokenStorage->getToken()->getUser();
if( $user instanceof User )
{
$user->setLastLogin( new \DateTime() );
$this->entityManager->flush();
}
}
P.S.
FosUserBundle uses interactive_login and a custom event to set last_login on entity
look at: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/EventListener/LastLoginListener.php#L63
Friend you can use this to inject the entityManager by a constructor
use Doctrine\ORM\EntityManagerInterface;
public function __construct(EntityManagerInterface $userManager){
$this->userManager = $userManager;
}
And in the checkPreAuth you call it
public function checkPreAuth(UserInterface $user){
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() ){
throw new AuthenticationException();
}else{
// BELOW IS WHAT I TRY, BUT FAIL.
$user->setLastLogin(new \DateTime());
$this->userManager->persist($user);
$this->userManager->flush();
}
}
I'm using HWIOAuthBundle to let an user login with Oauth, I've created a custom user provider which creates an user in case it doesn't exist:
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$attr = $response->getResponse();
switch($response->getResourceOwner()->getName()) {
case 'google':
if(!$user = $this->userRepository->findOneByGoogleId($attr['id'])) {
if(($user = $this->userRepository->findOneByEmail($attr['email'])) && $attr['verified_email']) {
$user->setGoogleId($attr['id']);
if(!$user->getFirstname()) {
$user->setFirstname($attr['given_name']);
}
if(!$user->getLastname()) {
$user->setLastname($attr['family_name']);
}
$user->setGoogleName($attr['name']);
}else{
$user = new User();
$user->setUsername($this->userRepository->createUsernameByEmail($attr['email']));
$user->setEmail($attr['email']);
$user->setFirstname($attr['given_name']);
$user->setLastname($attr['family_name']);
$user->setPassword('');
$user->setIsActive(true);
$user->setGoogleId($attr['id']);
$user->setGoogleName($attr['name']);
$user->addGroup($this->groupRepository->findOneByRole('ROLE_USER'));
$this->entityManager->persist($user);
}
}
break;
case 'facebook':
if(!$user = $this->userRepository->findOneByFacebookId($attr['id'])) {
if(($user = $this->userRepository->findOneByEmail($attr['email'])) && $attr['verified']) {
$user->setFacebookId($attr['id']);
if(!$user->getFirstname()) {
$user->setFirstname($attr['first_name']);
}
if(!$user->getLastname()) {
$user->setLastname($attr['last_name']);
}
$user->setFacebookUsername($attr['username']);
}else{
$user = new User();
$user->setUsername($this->userRepository->createUsernameByEmail($attr['email']));
$user->setEmail($attr['email']);
$user->setFirstname($attr['first_name']);
$user->setLastname($attr['last_name']);
$user->setPassword('');
$user->setIsActive(true);
$user->setFacebookId($attr['id']);
$user->setFacebookUsername($attr['username']);
$user->addGroup($this->groupRepository->findOneByRole('ROLE_USER'));
$this->entityManager->persist($user);
}
}
break;
}
$this->entityManager->flush();
if (null === $user) {
throw new AccountNotLinkedException(sprintf("User '%s' not found.", $attr['email']));
}
return $user;
}
The problem is that twitter for example doesn't give the email or i want some additional fields to be included before a new user is created. Is there a way to redirect an user to a "complete registration" form before creating it?
I've tried to add a request listener, that on each request, if the user is logged, checks if the email is there and if it doesn't it redirects to the complete_registration page, but it will redirect also if the user goes to the homepage, to logout or anything else, I want to redirect him only if he tries to access some user restricted pages.
Or better, don't create it until he gives all the required informations.
I've found the solution by myself, I've manually created a new exception:
<?php
namespace Acme\UserBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use HWI\Bundle\OAuthBundle\Security\Core\Exception\OAuthAwareExceptionInterface;
/**
* IncompleteUserException is thrown when the user isn't fully registered (e.g.: missing some informations).
*
* #author Alessandro Tagliapietra http://www.alexnetwork.it/
*/
class IncompleteUserException extends AuthenticationException implements OAuthAwareExceptionInterface
{
private $user;
private $accessToken;
private $resourceOwnerName;
/**
* {#inheritdoc}
*/
public function setAccessToken($accessToken)
{
$this->accessToken = $accessToken;
}
/**
* {#inheritdoc}
*/
public function getAccessToken()
{
return $this->accessToken;
}
/**
* {#inheritdoc}
*/
public function getResourceOwnerName()
{
return $this->resourceOwnerName;
}
/**
* {#inheritdoc}
*/
public function setResourceOwnerName($resourceOwnerName)
{
$this->resourceOwnerName = $resourceOwnerName;
}
public function setUser($user)
{
$this->user = $user;
}
public function getUser($user)
{
return $this->user;
}
public function serialize()
{
return serialize(array(
$this->user,
$this->accessToken,
$this->resourceOwnerName,
parent::serialize(),
));
}
public function unserialize($str)
{
list(
$this->user,
$this->accessToken,
$this->resourceOwnerName,
$parentData
) = unserialize($str);
parent::unserialize($parentData);
}
}
In this way, in the custom Oauth user provider when i check if an user exist or I create a new user i check if the required fields are missing:
if (!$user->getEmail()) {
$e = new IncompleteUserException("Your account doesn't has a mail set");
$e->setUser($user);
throw $e;
}
In that case the user will be redirected to the login form, with that exception in session, so in the login page I do:
if($error instanceof IncompleteUserException) {
$session->set(SecurityContext::AUTHENTICATION_ERROR, $error);
return $this->redirect($this->generateUrl('register_complete'));
}
And it will be redirected to a form with the $user in the exception so it can ask only for the missing information and then login the user.
I ran into a similar issue while migrating to a sf site. After the migration I wanted the migrated users to complete their profile whereas the newly registered users could get started immediately.
I solved this similar to your idea using a RequestListener but added a whitelist of pages that the user is allowed without completion of his profile. Depending on the number of pages you want the user to have access to without completion of his profile you might also consider using a blacklist.
I did not check for th existance of a particular field uppon redirecting to the profile completion page but added a role "ROLE_MIGRATION" when migrating the usersdatabase. In your case you can add the role when creating the user through oauth.
Here the code of my request listener:
public function onRequest(GetResponseEvent $evt)
{
if (HttpKernelInterface::MASTER_REQUEST !== $evt->getRequestType())
{
return;
}
$token = $this->securityContext->getToken();
if(!is_object($token)) or not migrating
{
// user is not logged in
return;
}
$user = $token->getUser();
if(!$user instanceof User || !$user->hasRole('ROLE_MIGRATING'))
{
// different user class or already migrated
return;
}
$openPaths = array(
'/start-2.0',
'/css',
'/js',
'/images',
'/media',
'/geo',
'/_wdt',
'/logout', or not migrating
'/terms',
'/contact',
'/about',
'/locale/set'
);
foreach($openPaths as $p)
{
if(strpos($evt->getRequest()->getPathInfo(),$p)===0)
{
// path is open for migrating users
return;
}
}
header('Location: /start-2.0');
exit;
}