Validate user account via mailbox symfony3 - php

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.

Related

chat with Symfony mercure Failed to send an update & Message does not added in the data base

i want to create a real time chat group with symfony mercure, this is my MessageController code but i have this error when i ping mercure to do the update "Failed to send an update", also the message does not added in the database what is the problem!!
Does anyone know what causes this issue and how to resolve it?
<?php
namespace App\Controller;
use App\Entity\GroupConversation;
use App\Entity\Message;
use App\Entity\User;
use App\Form\MessageType;
use App\Repository\MessageRepository;
use App\Repository\UserRepository;
use App\Service\CookieGenerator;
use Doctrine\ORM\EntityManagerInterface;
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\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Uid\Uuid;
/**
* #Route("/messages", name="messages_")
*/
class MessageController extends AbstractController
{
/**
* #var MessageRepository
*/
private $messageRepository;
/**
* #var UserRepository
*/
private UserRepository $userRepository;
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var Security
*/
private Security $security;
public function __construct(MessageRepository $messageRepository,
UserRepository $userRepository,
EntityManagerInterface $em,
Security $security)
{
$this->messageRepository = $messageRepository;
$this->userRepository = $userRepository;
$this->em = $em;
$this->security = $security;
}
//BREAD controller action pattern
/**
* Display list of messages from conversation
*
* #Route("/{groupConversation}", name="browse")
* #param GroupConversation $groupConversation
* #return Response
*/
public function browse(GroupConversation $groupConversation): Response {
$this->denyAccessUnlessGranted('ROLE_USER');
$messages = $this->messageRepository->findMessageByConversationId($groupConversation->getId());
$response = $this->render('message/browse.html.twig', [
'conversation' => $groupConversation,
'messages' => $messages,
]);
return $response;
}
/**
* Create new message
*
* #Route("/{id}/add", name="add", requirements={"id" : "\d+"})
*/
public function add(Request $request,
HubInterface $hub,
GroupConversation $groupConversation): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
//used with connected user
$user = $this->security->getUser();
if(!($user)) {
$this->addFlash('error', 'Utilisateur incorrect.');
return $this->redirectToRoute('app_login');
}
$message = new Message();
$form = $this->createForm(MessageType::class, $message);
$form->handleRequest($request);
$content = $request->get('message-box', null);
if ($content) {
$message->setCreated(new \DateTime('now'));
$message->setUpdated(new \DateTime('now'));
$message->setContent($content);
$message->setMine(true);
$message->setSeen(false);
$message->setUser($user);
$groupConversation->addMessage($message);
try {
$date = new \DateTime('now');
$update = new Update(
'/messages/' . $groupConversation->getId(), //IRI, the topic being updated, can be any string usually URL
json_encode([
'conversation' => 'Nouveau message conversation :' . $groupConversation->getName(),
'message' => $content,
'from' => $user->getUsername(),
'to' => $groupConversation->getUsers(),
'date' => $date->format('H:i'),
]), //the content of the update, can be anything
$groupConversation->getPrivate(), //private
'message-' . Uuid::v4(),//mercure id
'message'
);
//PUBLISHER JWT : doit contenir la liste des conversations dans lesquels il peut publier conf => mercure.publish
//SUBSCRIBER JWT: doit contenir la liste des conversations dans lesquels il peut recevoir conf => mercure.subcribe
$hub->publish($update);
$this->em->flush();
}
catch (\Exception $e) {
//dd($groupConversation);
throw $e;
}
}
return $this->redirectToRoute('messages_browse', ['groupConversation' => $groupConversation->getId()] );
}
/**
* Ping mercure
* #Route("/{id}/ping", name="ping")
*/
public function ping(Request $request, HubInterface $hub)
{
$this->denyAccessUnlessGranted('ROLE_USER');
$update = new Update(
'/ping/' . $request->get('id'), //IRI, the topic being updated, can be any string usually URL
json_encode(['message' => 'pinged !']), //the content of the update, can be anything
false, //private
'ping-' . Uuid::v4(), //mercure id
'ping'
);
$hub->publish($update);
return $this->redirectToRoute('messages_browse', ['groupConversation' => $request->get('id')]);
}
}
like so
I don't know what $groupConversation->addMessage() does internally, but I can't see where the $message or $groupConversation get persisted to the entity manager, so they can be added to the DB when it is flushed and have IDs generated if required.
Only after they are flushed, would I publish the update to the Mercure $hub.

How to manually authenticate user after Registration with the new Symfony 5 Authenticator?

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()
]);
}

Symfony Test - null token with Guard Authentication

I have a Guard Authentication in my Symfony Web Application. I would like to perform some unit tests. I'm unable to simulate an authentification in my tests. The token stays null when calling $tokenStorage->getToken().
Note:
The login authentification is working under dev and prod environnement.
I saw quite a lot of related topics without success and the doc.
Symfony version: 3.4.
Reproduce: you can reproduce the error from this repo (symfony project). This repo defined one entity User with a custom constraint validator ExampleValidator. In this constraint, I need to have the current logged user.
Code sample:
After manually creating an User, the login function used in tests:
private function logIn($firewallName = 'main'){
// dummy call to bypass the hasPreviousSession check
$crawler = $this->client->request('GET', '/');
$session = $this->client->getContainer()->get('session');
/** #var User $user */
$user = $this->entityManager->getRepository(User::class)
->findOneBy(['email' => 'user1#example.com']);
// you may need to use a different token class depending on your application.
// for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
$token = new PostAuthenticationGuardToken($user, $firewallName, [new Role('ROLE_CLIENT')]);
self::$kernel->getContainer()->get('security.token_storage')->setToken($token);
$session->set('_security_'.$firewallName, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
The User call from tokenStorage (from service function):
class ExampleValidator extends ConstraintValidator{
protected $requestStack;
protected $em;
protected $user_id;
public function __construct(RequestStack $request,
EntityManager $em,
TokenStorage $tokenStorage){
$this->requestStack = $request;
$this->em = $em;
/** #var User $user */
// Token is always null
$user = $tokenStorage->getToken()->getUser();
$this->user_id = $user != "anon." ? $user->getId() : null;
}
/**
* #param $value
* #param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
// validation rules ...
}
}
LoginFormAuthenticator.php
<?php
namespace AppBundle\Security;
use AppBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
private $loginAttemptRepository;
public function __construct(EntityManagerInterface $entityManager,
UrlGeneratorInterface $urlGenerator,
CsrfTokenManagerInterface $csrfTokenManager,
UserPasswordEncoderInterface $passwordEncoder){
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
/**
* #param Request $request
* #return bool
*/
public function supports(Request $request){
return $request->getPathInfo() == '/login_check' &&
$request->isMethod('POST') &&
$request->request->get('_password') !== null;
}
/**
* #param Request $request
* #return array|mixed|void|null
*/
public function getCredentials(Request $request){
$isLoginSubmit = $request->getPathInfo() == '/login_check' &&
$request->isMethod('POST') &&
$request->request->get('_password') !== null;
$isCaptcha = $request->request->get('captcha_set');
if ($isCaptcha == 1 && $request->request->get('_password') !== null) {
$secret = ...;
if($_POST['g-recaptcha-response'] !== null){
// Paramètre renvoyé par le recaptcha
$response = $_POST['g-recaptcha-response'];
$remoteip = $_SERVER['REMOTE_ADDR'];
$api_url = "https://www.google.com/recaptcha/api/siteverify?secret="
. $secret
. "&response=" . $response
. "&remoteip=" . $remoteip ;
$decode = json_decode(file_get_contents($api_url), true);
if ($decode['success'] == true) {
$username = $request->request->get('_username');
$password = $request->request->get('_password');
$csrfToken = $request->request->get('_csrf_token');
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
$request->getSession()->set(
Security::LAST_USERNAME,
$username
);
return [
'username' => $username,
'password' => $password,
];
}
else{
throw new CustomUserMessageAuthenticationException('Captcha invalids.');
}
}
else{
throw new CustomUserMessageAuthenticationException('Captcha invalids.');
}
}
else {
if (!$isLoginSubmit) {
// skip authentication
return;
}
$username = $request->request->get('_username');
$password = $request->request->get('_password');
$csrfToken = $request->request->get('_csrf_token');
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
$request->getSession()->set(
Security::LAST_USERNAME,
$username
);
return [
'username' => $username,
'password' => $password,
];
}
}
/**
* #param mixed $credentials
* #param UserProviderInterface $userProvider
* #return User|object|UserInterface|null
*/
public function getUser($credentials, UserProviderInterface $userProvider){
$username = $credentials["username"];
$user = $this->entityManager->getRepository(User::class)
->findOneBy(['username' => $username]);
return $user;
}
/**
* #param mixed $credentials
* #param UserInterface $user
* #return bool
*/
public function checkCredentials($credentials, UserInterface $user){
$password = $credentials["password"];
$rep = false;
if ($this->passwordEncoder->isPasswordValid($user, $password)){
$rep = true;
}
return $rep;
}
/**
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey
* #return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey){
$targetPath = null;
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('map'));
}
/**
* #return string
*/
protected function getLoginUrl(){
return $this->urlGenerator->generate('fos_user_security_login');
}
}
I believe the root of your problem is that you are using multiple container instances. In particular, your logIn() function works on the container of the client, but the validator is from a different container that you boot up during setUp(). Thus, the changes you make in logIn() to the client container do not affect the validator you are actually testing.
Using the same container everywhere, e.g. the one from the client, should solve this. The following changes to your repository make the test pass:
diff --git a/tests/AppBundle/Validator/UserTest.php b/tests/AppBundle/Validator/UserTest.php
index f15c854..603e566 100644
--- a/tests/AppBundle/Validator/UserTest.php
+++ b/tests/AppBundle/Validator/UserTest.php
## -44,10 +44,7 ## class UserTest extends WebTestCase{
$this->container = $this->client->getContainer();
$this->entityManager = $this->container->get('doctrine.orm.entity_manager');
- // Set validator
- $kernel = $this->createKernel();
- $kernel->boot();
- $this->validator = $kernel->getContainer()->get('validator');
+ $this->validator = $this->client->getContainer()->get('validator');
// Create one user
$this->createOneUser();
## -100,7 +97,7 ## class UserTest extends WebTestCase{
// you may need to use a different token class depending on your application.
// for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
$token = new PostAuthenticationGuardToken($user, $firewallName, [new Role('ROLE_CLIENT')]);
- self::$kernel->getContainer()->get('security.token_storage')->setToken($token);
+ $this->client->getContainer()->get('security.token_storage')->setToken($token);
$session->set('_security_'.$firewallName, serialize($token));
$session->save();

How to avoid returning user password in Symfony 3 via JSON

I am developing a Symfony app with a REST API integrated but I'm facing a problem, when returning an user entity as JSON through an API request it returns the user password and despite being encrypted I would like to avoid it.
My user entity is:
<?php
namespace AppBundle\Entity;
use AppBundle\Util\Language;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
/**
* #ORM\Table(name="users")
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User implements AdvancedUserInterface, \Serializable
{
public function __construct()
{
$this->isActive = true;
}
// Functions and parameters
/**
* Set password
*
* #param string $password
*
* #return User
*/
public
function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
// More functions and parameters
/** #see \Serializable::serialize() */
public
function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
$this->isActive,
$this->createdAt,
$this->lastLogin,
));
}
/** #see \Serializable::unserialize() */
public
function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
$this->isActive,
$this->createdAt,
$this->lastLogin,
) = unserialize($serialized);
}
}
The User repository
<?php
namespace AppBundle\Repository;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository implements UserLoaderInterface
{
public function loadUserByUsername($username)
{
return $this->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
->getOneOrNullResult();
}
}
I have an static method to build API responses
public static function createSuccessfulresponse($entity, $entityName, $responseCode, $userLocale = "en", $responseMsg = "")
{
$defResponseMsg = ($responseMsg != "" ? $responseMsg : ApiResponseCode::getMsg($responseCode, $userLocale));
$responseArray = array();
$responseArray['responseCode'] = $responseCode;
$responseArray['responseMsg'] = $defResponseMsg;
$responseArray['userLocale'] = $userLocale;
if ($entity != null) {
$responseArray[$entityName] = $entity;
}
return ApiResponseHelper::serializeResponse($responseArray);
}
The response serializer
private static function serializeResponse($responseArray)
{
$encoders = array(new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);
return $serializer->serialize($responseArray, 'json');
}
And one of the API calls which returns an user entity (there are more)
/**
* #Route("/api/url/{uid}" )
* #Method({"GET"})
*/
public function getByUidAction($uid)
{
$user = $this->get('security.token_storage')->getToken()->getUser();
$entityManager = $this->getDoctrine()->getManager();
$entity = $entityManager->getRepository('AppBundle:Workday')->findOneBy(['uid' => $uid, 'user' => $user]);
if($entity != null){
return new Response(ApiResponseHelper::createSuccessfulresponse($entity, "workday", ApiResponseCode::SUCCESS_FETCH_WORKDAY, $user->getLocale()));
}else{
return new Response(ApiResponseHelper::createSuccessfulresponse(null, "workday", ApiResponseCode::ERROR_EXISTS_WORKDAY, $user->getLocale()));
}
}
This is one JSON response from the above method
{
"responseCode": "successfulResponseCode",
"responseMsg": "Data received",
"userLocale": "es",
"workday": {
"id": 10,
... so many data
"job": {
"id": 11,
.. more json data
},
"user": {
"username": "amendez",
"password": "encrypted_password",
... more data
},
... and more data
}
}
As you can see I receive a JSON object with the user which contains its encrypted password and many other data, my goal is to avoid returning the password key and value.
Does somebody know how could I achieve it?
You would need to define the Serializer groups with the desired getters assigned. See: https://symfony.com/doc/current/components/serializer.html#attributes-groups.
The preferred (best-practice) method would be to assign the desired groups.
use Symfony\Component\Serializer\Annotation\Groups;
class User implements AdvancedUserInterface, \Serializable
{
/**
* #Groups({"api"})
* #return string
*/
public function getUsername()
{
return $this->username;
}
//...
/**
* Get password
* #return string
*/
public function getPassword()
{
return $this->password;
}
}
Edited for easier use of Serializer service
In your app/config/config.yml enable annotations, which in-turn enables the serializer service.
#config.yml
framework:
#...
serializer:
enable_annotations: true
Now you can call the Serializer service directly or use DI in your custom services.
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\HttpFoundation\JsonResponse;
private static function serializeResponse($responseArray)
{
$serializer = $this->container->get('serializer');
return $serializer->serialize($responseArray, JsonEncoder::FORMAT, array(
'groups' => array('api'),
'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS
));
}
To manually use the Serializer Component with groups.
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
private static function serializeResponse($responseArray)
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory);
$encoder = new JsonEncoder();
$serializer = new Serializer(array($normalizer), array($encoder));
return $serializer->serialize($responseArray, JsonEncoder::FORMAT, array('groups' => array('api')));
}
Alternatively you should be able to set it as part of the ignored attributes. See: https://symfony.com/doc/current/components/serializer.html#ignoring-attributes
private static function serializeResponse($responseArray)
{
$normalizer = new ObjectNormalizer();
$normalizer->setIgnoredAttributes(array('password'));
$encoder = new JsonEncoder();
$serializer = new Serializer(array($normalizer), array($encoder));
return $serializer->serialize($responseArray, JsonEncoder::FORMAT);
}

FOSUserBundle: how to allow registration, confirmation but not activation?

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();
}
}

Categories