symfony 3.1 registration email no FOSUser - symfony

Nice to meet you.
I'm developping with Symfony 3.1.3 and I'm using the security system offered by the framework, no FOSUser neither Guard.
I have in my controller the typical function login:
public function loginAction(Request $request)
{
// This works
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'security/loginForm_old.html.twig',
array(
// last username entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
}
And I want to check if the user has activated his account. In the entity User I have the isActive attribute set to false by default and only with the link into the registration email is setted to true.
I have been searching for this issue without results and I'm sure this is something very common, everybody wants to check if the user's email is a good one.
Thanks.

Lets assume that you have an RegistrationController.php class where you store all code that manage about user's registration.
Create a function which sends email to user after registration:
public function sendConfirmationEmailMessage(User $user)
{
$confirmationToken = $user->getConfirmationToken();
$username = $user->getUsername();
$subject = 'Account activation';
$email = $user->getEmail();
$renderedTemplate = $this->templating->render('AppBundle:Emails:registration.html.twig', array(
'username' => $username,
'confirmationToken' => $confirmationToken
));
$message = \Swift_Message::newInstance()
->setSubject($subject)
->setFrom(MAILER_FROM)
->setReplyTo(MAILER_FROM)
->setTo($email)
->setBody($renderedTemplate, "text/html");
$this->mailer->send($message);
}
Create a route associated with function which takes an generated token as argument, then search user by that token and activate if user exist:
/**
* #Route("user/activate/{token}")
*/
public function confirmAction(Request $request, $token)
{
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('AppBundle:User');
$user = $repository->findUserByConfirmationToken($token);
if (!$user)
{
throw $this->createNotFoundException('We couldn\'t find an account for that confirmation token');
}
$user->setConfirmationToken(null);
$user->setEnabled(true);
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_registration_confirmed');
}
Then when you have a function which actually registers the user you call the sendConfirmationEmailMessage as shown below:
public function registerAction(Request $request)
{
/* All the logic goes here: form validation, creating new user */
/* $user is created user */
sendConfirmationEmailMessage($user);
}
Anyway if isActive() function return false Symfony security system will prevent you from login. Your User entity should implement UserInterface.

Related

passwordEncoder generated hash for non-authentication purpose fails on verification

For a user to register with my system, I'm sending a confirmation mail with a generated link. On click the system should set an active property to true.
To create the confirm link, I generate a md5 hashed random_byte value. This value is part of the link I send to the user. The value is hashed with the passwordEncoder and stored in my database.
When the user clicks the link, the system checks the value against the hash in my database and should return true, to fully activate the user. For some reason the verification fails.
I checked, that the generated $identifier value in register() is the one that gets send to the user. They match. So no mistake there.
At first I wanted to use password_verify() only, but that didn't work out either.
I simply have no clue, why it's not working. My last guess is, that there is some Symfony dark magic going on, that I'm not aware of.
public function register(EntityManagerInterface $entityManager, Request $request, UserPasswordEncoderInterface $passwordEncoder, MailerInterface $mailer)
{
// Register-Form
$form = $this->createForm(RegisterFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var User $user */
$user = $form->getData();
$user->setPassword($passwordEncoder->encodePassword(
$user,
$form['plainPassword']->getData()
));
// create activation hash
$identifier = md5(random_bytes(16));
$user->setActiveHash(
$passwordEncoder->encodePassword(
$user,
$identifier
)
);
// send activation mail with activation hash
$message = (new TemplatedEmail())
->subject('...')
->from('...')
->to($user->getEmail())
->textTemplate('emails/registration.txt.twig')
->context([
'mail' => $user->getEmail(),
'identifier' => $identifier,
])
;
$mailer->send($message);
// persist & flush
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', '...');
//return $this->redirectToRoute('login');
return $this->render('home/index.html.twig');
}
return $this->render('security/register.html.twig', [
'registerForm' => $form->createView()
]);
}
/**
* #Route("/confirm/{email}/{identifier}", name="register.confirm")
*/
public function confirm($email, $identifier, EntityManagerInterface $entityManager, UserPasswordEncoderInterface $passwordEncoder)
{
$user = $entityManager->getRepository(User::class)->findOneBy([
'email' => $email,
'active' => false
]);
if (!$user) {
// fail no user with email
die('no such user');
}
if( !$passwordEncoder->isPasswordValid($user, $identifier) ){
// Hash verification failed
die('hash failed');
}
$user->setActive(true);
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', '...');
return $this->redirectToRoute('login');
}
EDIT:
I tried manually hashing a string with password_hash('a_string', PASSWORD_ARGON2ID) and it throws an exception that PASSWORD_ARGON2ID is an undefined constant. PASSWORD_ARGON2I works. And Symfony somehow uses Argon2ID as my password strings and the activeHash strings generated through encodePassword() start with "$argon2id..." How is that possible?
Ok. So I was running PHP 7.2 via MAMP locally. As (before it was deleted) mentioned in the comments by msg, Argon2ID hashing was probably provided by Sodium. 7.2 password_verify() on the other hand was not able to verify a argon2id hash.
Upgraded to PHP 7.3.7 and no problem anymore.

Unable to guess how to get a Doctrine instance from the request information for parameter "class"

I'm trying to send an email with a token in order to resend an automated password. But in my view i'm retrieving the following error:
/**
* #Route("/forget-email/", name="forget", methods="GET|POST")
*/
public function emailrestore(Request $request, User $user, \Swift_Mailer $mailer)
{
$url = "test";
$form = $this->createForm(ForgetPasswordType::class, $user);
$form->handleRequest($request);
$email = $form['email']->getData();
$user = $this->getDoctrine()
->getRepository(User::class)
->find($email);
if ( $email === $user ) {
$mail = (new \Swift_Message('Hello Email'))
->setFrom('email#email.email')
->setTo($email)
->setBody(
$this->renderView(
// templates/emails/registration.html.twig
'emails/registration.html.twig',array('url' => $url,)
),
'text/html'
);
$mailer->send($mail);
} else{
var_dump("$email");
}
return $this->render('forget/email.html.twig', [
'form' => $form->createView(),
'error' => null,
]);
}
Inside my entity i have email as unique
* #UniqueEntity(fields="email", message="Email already taken")
I'm retrieving the following error:
"Unable to guess how to get a Doctrine instance from the request information for parameter "user"." Why?
Thanks for your explanation for advance
You have "User $user" in your action parameter but you don't have a parameter 'user' in your request, my guess is your ParamConverter can't convert this one into a User object because of that.
make your route a GET route formatted like:
/**
* #Route("/forget-email/{user}", name="forget", methods="GET")
*/
Where user is whatever you want (mail, username, uid, etc ...) has long as it allows you tyo retrieve your user object and have your paramconverter converting it into a Doctrine object ...
You need to specify an initial value for your $user variable
Try this :
public function emailrestore(Request $request, User $user, \Swift_Mailer $mailer)

How can i get the user with id session in param request with symfony2

I'm trying to do a little API with Symfony2.
I send a session id to my controller with a URL like this:
localhost/symfony2/web/app_dev.php/users/getuser/c5auv7mrp45rnd046cfv0vgl96
Then, in Symfony,
/**
* #Route("/getuser/{sessionId}")
*/
public function getSessionAction(Request $request, $sessionId)
{
// Here is what i'm trying to do
$packJson = array(
'user_id' => $userid
);
$response = new JsonResponse();
$response->setData($packJson);
return $response;
}
So, i would like to retrieve my user Id only with the sessionId argument.
Of course, it will be load from Db
I don't understand the logic between Session object and User Objet
Thanks
I think you want to use a token to identify a user. That means you have one token for each user in your database. If that is correct then it has nothing to do with sessions or a session-object.
you could simple retrieve your user with:
/**
* #Route("/getuser/{token}")
*/
public function getSessionAction($token)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AdminBundle:User')->findOneBy(array('token' => $token);
$response = new JsonResponse();
if (!$entity) {
$response->setData('error' => 'bad token');
return $response;
}
$packJson = array(
'user_id' => $entity->getId()
);
$response->setData($packJson);
return $response;
}

Symfony 2 FosUser: Authenticate after registration

I'm using FOSUserBundle together with NnmMultiUserBundle.
When a user register, using my form_handler, the User entity is created, and should automatically login this new user.
I haven't been able to make that automatic login works.
My Controller:
public function registerAction()
{
$discriminator = $this->container->get('nmn_user_discriminator');
$discriminator->setClass('Travelyo\UserBundle\Entity\UserFrontend');
$form = $discriminator->getRegistrationForm();
//$form = $this->container->get('fos_user.registration.form');
$formHandler = $this->container->get('fos_user.registration.form.handler');
$confirmationEnabled = $this->container->getParameter('fos_user.registration.confirmation.enabled');
$process = $formHandler->process($confirmationEnabled, array(User::ROLE_CUSTOMER));
if ($process) {
$user = $form->getData();
if ($confirmationEnabled) {
$this->container->get('session')->set('fos_user_send_confirmation_email/email', $user->getEmail());
$route = 'fos_frontend_registration_check_email';
} else {
$this->authenticateUser($user);
$route = 'fos_frontend_registration_confirmed';
}
$this->setFlash('fos_user_success', 'registration.flash.user_created');
$url = $this->container->get('router')->generate($route);
return new RedirectResponse($url);
}
return $this->container->get('templating')->renderResponse('TravelyoUserBundle:Registration:user_frontend.form.html.' . $this->getEngine(), array('form' => $form->createView(),));
}
Athenticate method:
protected function authenticateUser(UserInterface $user)
{
try {
$this->container->get('fos_user.user_checker')->checkPostAuth($user);
} catch (AccountStatusException $e) {
// Don't authenticate locked, disabled or expired users
return;
}
$providerKey = $this->container->getParameter('fos_user.firewall_name');
$token = new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
$this->container->get('security.context')->setToken($token);
}
After calling athenticate the controller redirects to ConfirmedAction, where it tries to get the "logged in" user:
public function confirmedAction()
{
$user = $this->container->get('security.context')->getToken()->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
return $this->container->get('templating')->renderResponse('FOSUserBundle:Registration:confirmed.html.'.$this->getEngine(), array(
'user' => $user,
));
}
Error message:
Fatal error: Call to a member function getUser() on a non-object in /Data/Web/Local/travelyo/vendor/bundles/FOS/UserBundle/Controller/RegistrationController.php on line 115
If I then try to login using the login form it works fine (meaning that the creation of the user is working). But I can't understand why this token cannot be retrieved from security.context.
Security context is (as described in symfony 2 doc) not shared across firewall.
My registration form was in the "public firewall" => /register.
When authenticating the user (right after the user is registered), the token is saved for the public security.context.
I was then redirected to my actual "account" firewall => /account, and no token was found in the security.context as the token had never been defined for that firewall.
In order to solve it I simply moved my registration process under the same firewall, and it now works like a charm.
But I would still assume it should be possible to set a token for another security.context, if anyone know how it might be useful.
Thanks!

Manual authentication check Symfony 2

I'm working on a Symfony 2 application where the user must select a profile during the login process.
Users may have multiples profiles to work with and they only know their own profiles. So first, I need to prompt for username and password, if those are correct, I should not login the user, I need to prompt for profile witch user will use during the session.
So, I show a form with a username and password field, and send it using an Ajax request, that request responds with the profile list if username and password are correct or an error code otherwise. Finally the user logs into the system using username, password and profile.
The problem is that I don't know how to check if authentication data is correct (using all my authentication managers, users providers, etc) to accomplish this intermediate step (prompts for profile) without in fact logging the user.
Can anyone help me with this?
A problem with #Jordon's code is that it will not work with hashing algorithms that generate different hashes for the same password (such as bcrypt that stories internally its parameters, both the number of iterations and the salt). It is more correct to use isPasswordValid of the Encoder for comparing passwords.
Here is the improved code that works fine with bcrypt:
$username = trim($this->getRequest()->query->get('username'));
$password = trim($this->getRequest()->query->get('password'));
$em = $this->get('doctrine')->getManager();
$query = $em->createQuery("SELECT u FROM \Some\Bundle\Entity\User u WHERE u.username = :username");
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
if ($user) {
// Get the encoder for the users password
$encoder_service = $this->get('security.encoder_factory');
$encoder = $encoder_service->getEncoder($user);
// Note the difference
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
// Get profile list
} else {
// Password bad
}
} else {
// Username bad
}
You could do something like this to retrieve the user and manually test the password -
$username = trim($this->getRequest()->query->get('username'));
$password = trim($this->getRequest()->query->get('password'));
$em = $this->get('doctrine')->getEntityManager();
$query = $em->createQuery("SELECT u FROM \Some\Bundle\Entity\User u WHERE u.username = :username");
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
if ($user) {
// Get the encoder for the users password
$encoder_service = $this->get('security.encoder_factory');
$encoder = $encoder_service->getEncoder($user);
$encoded_pass = $encoder->encodePassword($password, $user->getSalt());
if ($user->getPassword() == $encoded_pass) {
// Get profile list
} else {
// Password bad
}
} else {
// Username bad
}
Once you've got your profile back from the client, you can perform the login manually in the AJAX server controller easily enough too -
// Get the security firewall name, login
$providerKey = $this->container->getParameter('fos_user.firewall_name');
$token = new UsernamePasswordToken($user, $password, $providerKey, $user->getRoles());
$this->get("security.context")->setToken($token);
// Fire the login event
$event = new InteractiveLoginEvent($this->getRequest(), $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
Might need a few use lines -
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
I used the code from #Jordon and #Potor Polak to wrap the logic in a standalone service that used the current access token to validate the password. Maybe some needs this:
services.yml:
app.validator.manual_password:
class: AppBundle\Service\ManualPasswordValidator
arguments:
- '#security.token_storage'
- '#security.encoder_factory'
ManualPasswordValidator.php:
<?php
namespace AppBundle\Service;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
/**
* Class ManualPasswordValidator
*
* #package AppBundle\Service
*/
class ManualPasswordValidator
{
/**
* #var EncoderFactory
*/
protected $encoderFactory;
/**
* #var TokenStorage
*/
protected $tokenStorage;
/**
* ManualPasswordValidator constructor.
*
* #param EncoderFactory $encoderFactory
* #param TokenStorage $tokenStorage
*/
public function __construct(TokenStorage $tokenStorage, EncoderFactory $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
$this->tokenStorage = $tokenStorage;
}
/**
* #param $password
* #return bool
*/
public function passwordIsValidForCurrentUser($password)
{
$token = $this->tokenStorage->getToken();
if ($token) {
$user = $token->getUser();
if ($user) {
$encoder = $this->encoderFactory->getEncoder($user);
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return true;
}
}
}
return false;
}
}
After this you can inject the ManualPasswordValidator wherever you want and use it like:
$password = $request->get('password');
$passwordIsValid = $this->manualPasswordValidator->passwordIsValidForCurrentUser($password);
The only way I could authenticate my users on a controller is by making a subrequest and then redirecting. Here is my code, I'm using silex but you can easily adapt it to symfony2:
$subRequest = Request::create($app['url_generator']->generate('login_check'), 'POST', array('_username' => $email, '_password' => $password, $request->cookies->all(), array(), $request->server->all());
$response = $app->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, false);
return $app->redirect($app['url_generator']->generate('curriculos.editar'));
In Symfony 4, the usage of the UserPasswordEncoderInterface is recommended in Controllers. Simply add a UserPasswordEncoderInterface as a parameter to the function in which you want to check the password and then add the code below.
public function changePasswordAction($old, $new, UserPasswordEncoderInterface $enc) {
// Fetch logged in user object, can also be done differently.
$auth_checker = $this->get('security.authorization_checker');
$token = $this->get('security.token_storage')->getToken();
$user = $token->getUser();
// Check for valid password
$valid = $encoder->isPasswordValid($user, $old);
// Do something, e.g. change the Password
if($valid)
$user->setPassword($encoder->encodePassword($user, $new));
}
Symfony 5.4
Password validation can be done using UserPasswordHasherInterface
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AuthenticaitonServices
{
public function __construct(UserPasswordHasherInterface $hasher)
{
$this->hasher = $hasher;
}
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
If anyone checking solution for password validation in Symfony 5.4.
Above code is for validating password posted from a login form.
Hope this is helpful.

Resources