I'd like the user to register, confirm it's email, but being activated manually by an administrator.
Thanks to this page I found the FOSUserEvents::REGISTRATION_CONFIRMED which is called right after clicking on the confirmation link in the email.
Now I'd like to disable the account (see below).
class RegistrationListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationCompleted'
);
}
public function onRegistrationCompleted(UserEvent $event) {
// registration completed
// TODO: disable the user. How?
}
}
Or is there any configuration that I missed?
Any ideas?
Thanks in advance!
As I can see, inside FOS\UserBundle\Controller\RegistrationController::
confirmAction() user is enabled:
/**
* Receive the confirmation token from user email provider, login the user.
*
* #param Request $request
* #param string $token
*
* #return Response
*/
public function confirmAction(Request $request, $token)
{
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
...
$user->setConfirmationToken(null);
$user->setEnabled(true);
$event = new GetResponseUserEvent($user, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRM, $event);
$userManager->updateUser($user);
...
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRMED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
I can think of two things you can do to disable it.
1) write an event listener, that will react on FOSUserEvents::REGISTRATION_CONFIRMED and disable the user => http://symfony.com/doc/master/bundles/FOSUserBundle/controller_events.html
2) override RegistrationController => https://symfony.com/doc/current/bundles/FOSUserBundle/overriding_controllers.html
I prefer first option.
class RegistrationListener implements EventSubscriberInterface
{
/** #var EntityManager */
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationCompleted'
);
}
public function onRegistrationCompleted(UserEvent $event) {
// registration completed
// TODO: disable the user. How?
$user = $event->getUser();
$user->setEnabled(false);
$this->em->persist($user);
$this->em->flush();
}
}
Related
Symfony 5 has changed its guard authentication method to a new Passport based one, using the new security config: enable_authenticator_manager: true;
I would like to know how to authenticate a user in the Registration form method in my controller, after the user is persisted by the ORM (Doctrine);
I have succeeded in authenticating the user using the login form, but I still do not know how to manually do this.
As per Cerad's comment, here is the full answer.
Below is only the part of the code related to the question & answer. These are not the full files.
Also, this is only for Symfony ^5.2 that is not using guard to authenticate the user.
/* config/packages/security.yaml */
security:
enable_authenticator_manager: true
firewalls:
main:
custom_authenticators:
- App\Security\SecurityAuthenticator
/* src/Security/SecurityAuthenticator.php */
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
/* automatically generated with the make:auth command,
the important part is to undestand that this is not a Guard implement
for the Authenticator class */
class SecurityAuthenticator extends AbstractLoginFormAuthenticator
{
}
/* src/Controller/RegistrationController.php */
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\SecurityAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
class RegistrationController extends AbstractController
{
/**
* #Route("/register", name="app_register")
*/
public function register(
Request $request,
UserPasswordEncoderInterface $passwordEncoder,
UserAuthenticatorInterface $authenticator,
SecurityAuthenticator $formAuthenticator): Response
{
/* Automatically generated by make:registration-form, but some changes are
needed, like the auto-wiring of the UserAuthenticatorInterface and
SecurityAuthenticator */
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword($passwordEncoder->encodePassword($user, $form->get('password')->getData()));
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// substitute the previous line (redirect response) with this one.
return $authenticator->authenticateUser(
$user,
$formAuthenticator,
$request);
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
}
For Symfony 6 find working solution, based on #Cerad's comment about UserAuthenticatorInterface::authenticateUser().
I declared my RegisterController in services.yaml with important argument (it is the reason):
App\Controller\RegisterController:
arguments:
$authenticator: '#security.authenticator.form_login.main'
So my RegisterController now looks like:
class RegisterController extends AbstractController
{
public function __construct(
private FormLoginAuthenticator $authenticator
) {
}
#[Route(path: '/register', name: 'register')]
public function register(
Request $request,
UserAuthenticatorInterface $authenticatorManager,
): RedirectResponse|Response {
// some create logic
...
// auth, not sure if RememberMeBadge works, keep testing
$authenticatorManager->authenticateUser($user, $this->authenticator, $request, [new RememberMeBadge()]);
}
}
Symfony 5.3 it's work for me
public function register(Request $request, Security $security, UserPasswordEncoderInterface $passwordEncoder, EventDispatcherInterface $dispatcher) {
......
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->get("security.token_storage")->setToken($token);
$event = new SecurityEvents($request);
$dispatcher->dispatch($event, SecurityEvents::INTERACTIVE_LOGIN);
return $this->redirectToRoute('home');
Here's my go at it, allowing you to authenticate a user, and also attach attributes to the generated token:
// src/Service/UserService.php
<?php
namespace App\Service;
use App\Entity\User;
use App\Security\LoginFormAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class UserService
{
private AuthenticatorInterface $authenticator;
private TokenStorageInterface $tokenStorage;
private EventDispatcherInterface $eventDispatcher;
// This (second parameter) is where you specify your own authenticator,
// if you have defined one; or use the built-in you're using
public function __construct(
LoginFormAuthenticator $authenticator,
TokenStorageInterface $tokenStorage,
EventDispatcherInterface $eventDispatcher
) {
$this->authenticator = $authenticator;
$this->tokenStorage = $tokenStorage;
$this->eventDispatcher = $eventDispatcher;
}
/**
* #param User $user
* #param Request $request
* #param ?array $attributes
* #return ?Response
*
*/
public function authenticate(User $user, Request $request, array $attributes = []): ?Response
{
$firewallName = 'main';
/** #see AuthenticatorManager::authenticateUser() */
$passport = new SelfValidatingPassport(
new UserBadge($user->getUserIdentifier(), function () use ($user) {
return $user;
})
);
$token = $this->authenticator->createAuthenticatedToken($passport, $firewallName);
/** #var TokenInterface $token */
$token = $this->eventDispatcher->dispatch(
new AuthenticationTokenCreatedEvent($token, $passport)
)->getAuthenticatedToken();
$token->setAttributes($attributes);
/** #see AuthenticatorManager::handleAuthenticationSuccess() */
$this->tokenStorage->setToken($token);
$response = $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
if ($this->authenticator instanceof InteractiveAuthenticatorInterface && $this->authenticator->isInteractive()) {
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
}
$this->eventDispatcher->dispatch(
$loginSuccessEvent = new LoginSuccessEvent(
$this->authenticator,
$passport,
$token,
$request,
$response,
$firewallName
)
);
return $loginSuccessEvent->getResponse();
}
}
Largely inspired from AuthenticatorManager::authenticateUser() and AuthenticatorManager::handleAuthenticationSuccess().
This might work depending on your setup. Note that main in the authenticateUserAndHandleSuccess() method is the name of my firewall in config/packages/security.yaml and LoginFormAuthenticator is the authenticator I created using bin/console make:auth.
/**
* #Route("/register", name="app_register")
* #param Request $request
* #param EntityManagerInterface $entityManager
* #param GuardAuthenticatorHandler $handler
* #param LoginFormAuthenticator $authenticator
* #param UserPasswordEncoderInterface $encoder
*
* #return Response
*/
public function register(
Request $request, EntityManagerInterface $entityManager, GuardAuthenticatorHandler $handler,
LoginFormAuthenticator $authenticator, UserPasswordEncoderInterface $encoder
): Response {
$user = new User();
$form = $this->createForm(RegisterType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$plainPassword = $form->get('plainPassword')->getData();
$user->setPassword($encoder->encodePassword($user, $plainPassword));
$entityManager->persist($user);
$entityManager->flush();
$handler->authenticateUserAndHandleSuccess($user, $request, $authenticator, 'main');
}
return $this->render('security/register.html.twig', [
'form' => $form->createView()
]);
}
I've an entity with a plainPassword and a password attribute. In form, I map on the plainPassword. After, when the user valid the form, I do password validation on the plainPassword.
To encode the password, I use an EventSubscriber that listen on prePersist and preUpdate. It works well for the register form, because it's a new entity, the user fill some persisted attributes, then doctrine persist it and flush.
But, when I just want to edit the password, it doesn't work, I think it's because the user just edit a non persisted attribute. Then Doctrine doesn't try to persist it. But I need it, to enter in the Subscriber.
Someone know how to do it ? (I've a similar problem in an other entity) For the moment, I do the operation in the controller...
Thanks a lot.
My UserSubscriber
class UserSubscriber implements EventSubscriber
{
private $passwordEncoder;
private $tokenGenerator;
public function __construct(UserPasswordEncoder $passwordEncoder, TokenGenerator $tokenGenerator)
{
$this->passwordEncoder = $passwordEncoder;
$this->tokenGenerator = $tokenGenerator;
}
public function getSubscribedEvents()
{
return array(
'prePersist',
'preUpdate',
);
}
public function prePersist(LifecycleEventArgs $args)
{
$object = $args->getObject();
if ($object instanceof User) {
$this->createActivationToken($object);
$this->encodePassword($object);
}
}
public function preUpdate(LifecycleEventArgs $args)
{
$object = $args->getObject();
if ($object instanceof User) {
$this->encodePassword($object);
}
}
private function createActivationToken(User $user)
{
// If it's not a new object, return
if (null !== $user->getId()) {
return;
}
$token = $this->tokenGenerator->generateToken();
$user->setConfirmationToken($token);
}
private function encodePassword(User $user)
{
if (null === $user->getPlainPassword()) {
return;
}
$encodedPassword = $this->passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($encodedPassword);
}
My user Entity:
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="email", type="string", length=255, unique=true)
* #Assert\NotBlank()
* #Assert\Email()
*/
private $email;
/**
* #Assert\Length(max=4096)
*/
private $plainPassword;
/**
* #ORM\Column(name="password", type="string", length=64)
*/
private $password;
ProfileController:
class ProfileController extends Controller
{
/**
* #Route("/my-profile/password/edit", name="user_password_edit")
* #Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
public function editPasswordAction(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(ChangePasswordType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Encode the password
// If I decomment it, it's work, but I want to do it autmaticlally, but in the form I just change the plainPassword, that is not persisted in database
//$password = $this->get('security.password_encoder')->encodePassword($user, $user->getPlainPassword());
//$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success', 'Your password have been successfully changed.');
return $this->redirectToRoute('user_profile');
}
return $this->render('user/password/edit.html.twig', [
'form' => $form->createView(),
]);
}
}
You can force Doctrine to mark an object as dirty by manipulating the UnitOfWork directly.
$em->getUnitOfWork()->scheduleForUpdate($entity)
However, I do strongly discourage this approach as it violates carefully crafted abstraction layers.
I'm using FOSUser Bundle to login. Now if user is already loggedin, how can I redirect user to homepage ('/'), if user visit to /login url.
I have copied SecurityController to src\AppBundle\Controller location and changed renderlogin method but it doesn't work.
renderLogin() method
protected function renderLogin(array $data)
{
if (false === $this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_ANONYMOUSLY')) {
return new RedirectResponse('/', 403);
}
return $this->render('#FOSUser/Security/login.html.twig', $data);
}
I have added this line as well in the security controller,
use Symfony\Component\HttpFoundation\RedirectResponse;
Any help is much appreciated.
Well you need to make some changes in the SecurityController
/**
* Renders the login template with the given parameters. Overwrite this function in
* an extended controller to provide additional data for the login template.
*
* #param array $data
*
* #return Response
*/
protected function renderLogin(array $data)
{
/**
* If the user has already logged in (marked as is authenticated fully by symfony's security)
* then redirect this user back (in my case, to the dashboard, which is the main entry for
* my logged in users)
*/
if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('homepage');
}
return $this->render('#FOSUser/Security/login.html.twig', $data);
}
}
And to redirect authenticated users who try to visit the registration page, you need to change the ``
class RegistrationController extends BaseController
{
/**
* #param Request $request
*
* #return Response
*/
public function registerAction(Request $request)
{
/**
* If the user has already logged in (marked as is authenticated fully by symfony's security)
* then redirect this user back (in my case, to the dashboard, which is the main entry for
* my logged in users)
*/
if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('homepage');
}
/** #var $formFactory FactoryInterface */
$formFactory = $this->get('fos_user.registration.form.factory');
/** #var $userManager UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
/** #var $dispatcher EventDispatcherInterface */
$dispatcher = $this->get('event_dispatcher');
$user = $userManager->createUser();
$user->setEnabled(true);
$event = new GetResponseUserEvent($user, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, $event);
if (null !== $event->getResponse()) {
return $event->getResponse();
}
$form = $formFactory->createForm();
$form->setData($user);
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$event = new FormEvent($form, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);
$userManager->updateUser($user);
if (null === $response = $event->getResponse()) {
$url = $this->generateUrl('fos_user_registration_confirmed');
$response = new RedirectResponse($url);
}
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
$event = new FormEvent($form, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_FAILURE, $event);
if (null !== $response = $event->getResponse()) {
return $response;
}
}
return $this->render('#FOSUser/Registration/register.html.twig', array(
'form' => $form->createView(),
));
}
}
It's weird that this has not been fixed in the FOSUserBundle, anyway I had two Github Gists that handles this issue:
Registration : https://gist.github.com/teeyo/147f2d5d21d1beadce133a51b02d9570
Login : https://gist.github.com/teeyo/121e21b35d71a9ab4a8f321043b6f6cd
In the FOS securityController (for login) : add this to login action
if($session->get("_security_main"))
{
$route = $this->container->get('router')->generate('site_faim_homepage');
return new RedirectResponse($route);
}
I just ask if user instance already exists and if yes, then I redirect to desired route (sorry if I somehow duplicated already added post, this is my fresh experience):
if($this->getUser()){
return $this->redirectToRoute('homepage');
}
I would like to do a registration email validation when a member clicks on a link in his mailbox with symfony3 ... without using FOSuserBundle
I add 2 fields in my user entity, a boolean $ validMail attribute, and a string code_validation to generate a random number on each enrollment.
By default, I initialize $validMail to false, I generate a random code in my entity user with each registration and i would like to detect when the user validates the email.
Here is my service :
namespace AppBundle\Services;
use AppBundle\Entity\User;
use AppBundle\Form\InscriptionType;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\FormFactory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder;
use Symfony\Component\Security\Core\Tests\Encoder\PasswordEncoder;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Securite implements ContainerAwareInterface
{
private $doctrine;
private $form;
private $session;
private $authenticationUtils;
/**
* #var ContainerInterface
*/
private $container;
/**
* #var PasswordEncoder
*/
private $passwordEncoder;
/**
* #var \Swift_Mailer
*/
private $mailer;
private $twig;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function __construct(EntityManager $doctrine, Session $session, FormFactory $form, AuthenticationUtils $authenticationUtils, UserPasswordEncoder $passwordEncoder, \Swift_Mailer $mailer, \Twig_Environment $twig)
{
$this->doctrine = $doctrine;
$this->session = $session;
$this->form = $form;
$this->authenticationUtils = $authenticationUtils;
$this->passwordEncoder = $passwordEncoder;
$this->mailer = $mailer;
$this->twig = $twig;
}
public function register(Request $request)
{
$inscription = new User();
$form = $this->form->create(InscriptionType::class, $inscription);
if ($request->isMethod('POST') && $form->handleRequest($request)->isValid())
{
$data = $form->getData();
$this->session->set('inscription', $data);
$inscription->setValidMail(false);
$password = $inscription->getPassword();
$this->cryptPassword($inscription, $password);
$this->doctrine->persist($inscription);
$this->doctrine->flush();
$this->session->getFlashBag()->add("success", "inscription confirmée, veuillez confirmer votre email");
$this->isValidMail();
}
return $form;
}
public function cryptPassword(User $user, $plainPassword)
{
$encoder = $this->passwordEncoder;
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
public function isValidMail()
{
$inscription = $this->session->get('inscription');
if ($inscription) {
$message = \Swift_Message::newInstance()
->setCharset('UTF-8')
->setSubject('Veuillez confirmer votre inscription')
->setBody($this->twig->render('back/email_inscription.html.twig', array(
'username' => $inscription->getUsername(),
'password' => $inscription->getPassword(),
'code_validation' => $inscription->getCodeValidation(),
)))
->setContentType('text/html')
->setFrom('test#gmail.com')
->setTo($inscription->getUsername());
$this->mailer->send($message);
}
}
}
I do not know how to detect when the user clicks on the link in his mailbox, refresh the page...I try to follow tutorials but it seems too complicated. What is the simplest way?
Why not to pass token in request ($_GET parameter). For example url:
www.blah.com/confirm_register?token=asdawddazvaefas
then take that token:
$token = $request->attributes->get('token');
and then check if that token exists in database if exists update that user field $validEmail to true.
Validation link has to include code_validation as URL parameter. Then you create new route for email validation.
When user clicks the link in the mailbox he visits the email validation route with the parameter. Then you validate GET parameter as Elimsas says.
I'm using Fosuserbundle and I want to render new template contain successfully message after resetting password. but I don't know how can I override the ResettingController file in order to render the template. here is the controller :
public function resetAction(Request $request, $token)
{
/** #var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */
$formFactory = $this->get('fos_user.resetting.form.factory');
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
/** #var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */
$dispatcher = $this->get('event_dispatcher');
$user = $userManager->findUserByConfirmationToken($token);
if (null === $user) {
throw new NotFoundHttpException(sprintf('The user with "confirmation token" does not exist for value "%s"', $token));
}
$form = $formFactory->createForm();
$form->setData($user);
$form->handleRequest($request);
$form->getErrors(true);
if ($form->isValid()) {
$event = new FormEvent($form, $request);
$dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_SUCCESS, $event);
$this->render('FOSUserBundle:Resetting:reset.html.twig', array(
'token' => $token,
'form' => $form->createView(),
));
$userManager->updateUser($user);
if (null === $response = $event->getResponse()) {
$url = $this->generateUrl('fos_user_profile_show');
$response = new RedirectResponse($url);
}
$dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_COMPLETED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
return $this->render('FOSUserBundle:Resetting:reset.html.twig', array(
'token' => $token,
'form' => $form->createView(),
));
}
I'm not using FosUserBundle and I'm not sure your code snippet abow is origin Fos controller. In your case, I don't think you needs to override the controller. Just listen to FOSUserEvents::RESETTING_RESET_SUCCESS event, create your own response and set back to the event $event->setResponse($response), your response will be used by Fos controller. When create your response, you can render any template path with any parameters as you want.
Here is an example:
class ResettingListener implements EventSubscriberInterface
{
/**
* #var EngineInterface
*/
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return [FOSUserEvents::RESETTING_RESET_SUCCESS => 'onResettingResetSuccess'];
}
/**
* #param FormEvent $event
*/
public function onResettingResetSuccess(FormEvent $event)
{
$response = $this->templating->render('YourBundle:Location:resetting.html.twig', [
'form' => $event->getForm()->createView()
]);
$event->setResponse($response);
}
}
And register as service:
<service id="your_bundle.listener.resetting" class="Vendor\YourBundle\EventListener\ResettingListener">
<argument type="service" id="templating" />
<tag name="kernel.event_subscriber" />
</service>