I'm trying to create a fosuserbundle for a quite strange use case, which is mandatory requirement, so no space to diplomacy.
Use case is as follow:
users in a mongo db table populated by jms messages -no registration form
users log in by ldap
user record not created by ldap, after a successful login username is checked against mongodb document
Considering that ldap could successfully log in people that exhist in ldap but cannot access site (but login is still successful), what could be the best way to perform such authentication chain?
I was thinking about some possible options:
listen on interactive login event, but imho there's no way to modify an onSuccess event
create a custom AuthenticationListener to do another check inside onSuccess method
chain authentication using scheb two-factor bundle
any hint?
I've used Fr3DLdapBundle which can be incorporate with FOSUserBundle quite easily (I'm using the 2.0.x version, I have no idea if the previous ones will do the same or be as easy to set up).
In the LdapManager (by default) it creates a new user if one is not already on the database which is not what I wanted (and doesn't seem to be what you want) so I have added my own manager that checks for the presence of the user in the database and then deals with the accordingly.
use FR3D\LdapBundle\Ldap\LdapManager as BaseLdapManager;
.. Other use stuff ..
class LdapManager extends BaseLdapManager
{
protected $userRepository;
protected $usernameCanonicalizer;
public function __construct(
LdapDriverInterface $driver,
$userManager,
array $params,
ObjectRepository $userRepository,
CanonicalizerInterface $usernameCanonicalizer
) {
parent::__construct($driver, $userManager, $params);
$this->userRepository = $userRepository;
$this->usernameCanonicalizer = $usernameCanonicalizer;
}
/**
* {#inheritDoc}
*/
public function findUserBy(array $criteria)
{
$filter = $this->buildFilter($criteria);
$entries = $this->driver->search(
$this->params['baseDn'], $filter, $this->ldapAttributes
);
if ($entries['count'] > 1) {
throw new \Exception('This search can only return a single user');
}
if ($entries['count'] == 0) {
return false;
}
$uid = $entries[0]['uid'][0];
$usernameCanonical = $this->usernameCanonicalizer->canonicalize($uid);
$user = $this->userRepository->findOneBy(
array('usernameCanonical' => $usernameCanonical)
);
if (null === $user) {
throw new \Exception('Your account has yet to be set up. See Admin.');
}
return $user;
}
Related
I am running a Symfony 2.8 based web app using FOSUserBundle to manage users. Creating new users with a web form is absolutely no problem.
Now I would like to add a feature to create new users with a REST api. Of course submitting username, password, email, etc. to a controller is no big deal:
public function registerAction(Request $request) {
$requestJson = json_decode($request->getContent(), true);
$username = $requestJson[JSON_KEY_USERNAME];
$email = $requestJson[JSON_KEY_MAIL];
$password = $requestJson[JSON_KEY_PASSWORD];
...
$this->registerUser($username, $email, $password);
...
}
private function registerUser($username, $email, $password, $locale, $timezone, $currency) {
$userManager = $this->get('fos_user.user_manager');
$emailExist = $userManager->findUserByEmail($email);
$userNameExists = $userManager->findUserByUsername($username);
if ($emailExist || $userNameExists)
return false;
$user = $userManager->createUser();
$user->setUsername($username);
$user->setEmail($email);
$user->setPlainPassword($password);
...
$user->setLocked(0);
$user->setEnabled(0);
$userManager->updateUser($user);
return true;
}
However, this performs no validation at all. If for example the username is empty an NotNullConstraintViolationException is thrown when persisting the user object.
Of course I could manually re-implement the same validation process which is used by the RegistrationForm (username not empty, not taken, no invalid characters, password min length, e-mail format, etc.) and pass back the same error messages but this would mean to reinvent the wheel.
Is it somehow possible to run the exact same validation which is used by the RegistrationForm?
Symfony validator can work independently. In a controller you can use validator service like this:
$violations = $this->get('validator')->validate($user, null, ['your_groups_here']);
// Or inject Symfony\Component\Validator\Validator\ValidatorInterface into a service.
It will return a ConstraintViolationListInterface, you can loop trough this object.
You can check FOSUserBundle validation groups here: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/config/validation.xml
I created an action that handles redirection to respected areas depending on user's type and ROLE (trainee, company or university let's say). If user is not logged in, it redirects to homepage (anonymous area), and if logged in - to their profile pages. I use it in homepage and many other cases, for example, as sign up and login success redirection.
public function authorizationAction(Request $request)
{
$user = $this->getUser();
$authorizationChecker = $this->get('security.authorization_checker');
$request->cookies->remove('action');
if ($user) {
if ($user->getType() == 'company' && $authorizationChecker->isGranted('ROLE_COMPANY_GUEST')) {
/** #var Company $company */
$company = $user->getCompany();
if ($user->getState() == 'active' && $company->getState() == 'active') {
$response = $this->redirectToRoute('company');
} else {
$response = $this->redirectToRoute('company_profile');
}
} elseif ($user->getType() == 'university' && $authorizationChecker->isGranted('ROLE_UNIVERSITY_GUEST')) {
/** #var University $university */
$university = $user->getUniversity();
if ($user->getState() == 'active' && $university->getState() == 'active') {
$response = $this->redirectToRoute('university');
} else {
$response = $this->redirectToRoute('university_profile');
}
} elseif ($user->getType() == 'trainee' && $authorizationChecker->isGranted('ROLE_TRAINEE')) {
/** #var Trainee $trainee */
$trainee = $user->getTrainee();
if ($user->getState() == 'active' && $trainee->getState() == 'active') {
$response = $this->redirectToRoute('trainee');
} else {
$response = $this->redirectToRoute('trainee_profile');
}
} else {
$response = $this->redirectToRoute('homepage');
}
} else {
$response = $this->redirectToRoute('homepage');
}
return $response;
}
I have seen some examples recommending using symfony events (kernel.request) to handle it minimizing controller code. But in this case I will not be able to use this action as sign up and login success path.
I am not using FOS, because of lack of user customization. I prefer handling User my self.
Is my approach wrong and something to be worried about?
Some things that I am concerned:
Redirection count:
For example. I am logged in user and I go to homepage and am redirected to my action where I am checked whether I am logged in or not and depending on user type I am redirected to respected page.
public function indexAction(Request $request)
{
if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('authorization');
}
// ...
}
Slowing website:
In the future I will be using this action in more pages and website will definatelly slow down everytime executing same code on each page.
I am not using FOS, because of lack of user customization. I prefer handling User my self.
Blockquote
You really shouldn't reinvent the wheel here but even if you want to your assessment that extending FOS is hard is wrong, take a look at this gist how many of us Symfony Devs extend FOS to enable social login via HWIOauthBundle. Extending it for other purposes is equally trivial.
Update Since OP's Comments Below
....
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use AppBundle\Form\Type\LoginType;
....
....
$login_form = $this->createForm(LoginType::class);
$login_form->handleRequest($request);
if (!$login_form->isValid()){
//throw error
}
$email = strip_tags($login_form->get('email')->getData());
$password = $login_form->get('password')->getData();
// Call the User Manager
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->findUserByUsernameOrEmail($email);
if(!$user){
//throw error : User With Submitted Credentials Does Not Exist
}
$user_password = $user->getPassword();
$encoded_password = $this->get('security.password_encoder')->encodePassword($user, $password);
if($encoded_password != $user_password){
//throw error : Wrong Password, Please Check Your Details & Re-submit
}
// Successful query of username/email and password now we log in user
// Create new token
$token = new UsernamePasswordToken($user, $user->getPassword(), 'main', $user->getRoles());
// Login New User
$tokenStorage = $this->get('security.token_storage');
$tokenStorage->setToken($token);
// User now logged in
$user_id = $user->getId();
....
Redirection count:
In your case each redirection would cause at least 1 database query to verify user session, so yes many them could lead to a bad use and server experience. If you use FOSUserBundle with Doctrine's Second Level Cache, you can avoid this query everytime you call $this->getUser();
Slowing website:
This very small bit of code with hardly be you bottleneck at scale but for arguments sake let's assume it is. I would solve this problem by introducing client side sessions. A framework like Angular or my personal favorite Ember would allow you to store user sessions on the client so as not to even bother going back to your server all the time for such a menial task as checking authentication or roles. I would still advise you to keep some server side code for those cheeky users who want to poke holes in your code.
The Access Control section in the Symfony documentation might offer easier solutions to restrict access. In my time using Symfony I have always been able to use it for redirection and access control.
I would like to pass the authenticated users list of roles to my front end apps, so I can use the same access control structure in the front and back end.
I was looking in the security / authentication classes as that is where the isGranted function are for me to do this
$this->container->get('security.context')->isGranted('ROLE_SUPER_ADMIN')
I can't find anything to get a list of roles though, is this not a supported feature?
nb: I don't want the entire role hierarchy, just the list of roles for the authenticated user
I ended up adding a new repository function and a service method to get this info.
MyProject/UserBundle/Entity/Repository/UserRepository
public function getRoles($userId)
{
$queryBuilder = $this->createQueryBuilder('u');
$queryBuilder
->select('u.id, u.roles AS user_roles, g.roles AS group_roles')
->leftJoin('u.groups', 'g')
->andWhere('u.id = :user_id')
->setParameter('user_id', $userId);
return $queryBuilder->getQuery()->getArrayResult();
}
MyProject/UserBundle/Service/UserService
public function getUserRoles($user)
{
$groupRoles = $this->repository->getRoles($user->getId());
$roles = array('user_roles' => array(), 'group_roles' => array());
foreach ($groupRoles as $groupRole) {
$roles['user_roles'] = array_merge($roles['user_roles'], $groupRole['user_roles']);
$roles['group_roles'] = array_merge($roles['group_roles'], $groupRole['group_roles']);
}
return $roles;
}
This gives me an array like this
"roles":{
"user_roles":[],
"group_roles":["ROLE_ADMIN","ROLE_ONE","ROLE_TWO","ROLE_BEST"]
}
Assuming you're using the Symfony security component, the user interface which your user class implements has this already included:
$user = $this->get('security.token_storage')->getToken()->getUser();
var_dump($user->getRoles());
http://api.symfony.com/3.1/Symfony/Component/Security/Core/User/UserInterface.html#method_getRoles
We are currently using Symfony 2 and FOS/UserBundle for user authentication.
I want to check if a given username/password combination is valid without logging in. This is because another person is currently logged in but for example needs to do a specific action which needs to be done by someone with a higher clearance.
Basically I want another user to do a different controller action besides the person that is currently logged.
If there's a better way of doing this please let me know
How can validate username and password from controller #696
public function validUser($username, $password){
$user = new Users(); //entity
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$bool = $encoder->isPasswordValid($user->getPassword(),$password,$user->getSalt());
}
Symfony 5.4
Password validation can be done using UserPasswordHasherInterface
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AuthenticaitonServices
{
public function __construct(UserPasswordHasherInterface $passwordHasher)
{
$this->hasher = $passwordHasher;
}
public function validate($request)
{
$form = [
"username" => $request->request->get("_username"),
"password" => $request->request->get("_password")
];
if(!$this->hasher->isPasswordValid($user, $form['password']))
{
// Incorrect Password
} else {
// Correct Password
}
isPasswordValid returns a bool response
Bundles are not longer available in newer Symfony versions. Above code is for validating password posted from a login form
Hope this is helpful.
I just started using Symfony's ACL system and was wondering why UserSecurityIdentity uses the username instead of the id of a User object to determine it's identity?
$user = new User();
$user->setId(new \MongoId());
$user->setUsername("frodo");
$dm->persist($user);
$uid = UserSecurityIdentity::fromAccount($user); // uses "frodo"
Our system allows users to alter their username, so using something more permanent (like the ID) to determine a user's identity seems more appropriate to me. Why was the ACL system implemented to use the username and not the ID? Any security considerations here?
This issue has been discussed here:
https://github.com/symfony/symfony/issues/5787
And has been solved in this commit:
https://github.com/symfony/symfony/commit/8d39213f4cca19466f84a5656a199eee98602ab1
So, now, whenever a user alter it's username, you can update its security indentity. I use a listener to do this:
public function preUpdate(PreUpdateEventArgs $eventArgs)
{
/** Update user security identity in case nick is changed * */
if ($entity instanceof \Acme\UserBundle\Entity\User && $eventArgs->hasChangedField('username')) {
$aclProvider = $this->container->get('security.acl.provider');
$securityId = UserSecurityIdentity::fromAccount($entity);
$aclProvider->updateUserSecurityIdentity($securityId, $eventArgs->getOldValue('username'));
}
}
This is implementation of fromAccount() method from Symfony\Component\Security\Acl\Domain\UserSecurityIdentity class:
public static function fromAccount(UserInterface $user)
{
return new self($user->getUsername(), ClassUtils::getRealClass($user));
}
I think it is answer on your question.