Symfony Test - null token with Guard Authentication - php

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

Related

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 4.2 unit testing wont works

I am doing unit testing / functional testing, but my code gives me an error. Kindly have a look on my code and suggest a solution.
Codes On Controller
<?php
namespace App\Controller;
use App\Entity\Organization;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Psr\Log\LoggerInterface;
use Swift_Mailer;
use Swift_Message;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
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\Validator\Validator\ValidatorInterface;
class RegisterController extends AbstractController
{
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Validate the data
* Encodes the password
* Creates new organization if evaplyId does not exists
* Create user and send verification email.
* return status
*
* #param Request $request
* #param UserPasswordEncoderInterface $userPasswordEncoder
* #param ValidatorInterface $validator
* #param Swift_Mailer $mailer
* #return Response
*/
public function register(Request $request, UserPasswordEncoderInterface $userPasswordEncoder, ValidatorInterface $validator, Swift_Mailer $mailer)
{
try {
$form = $request->getContent();
$form = json_decode($form);
$entityManager = $this->getDoctrine()->getManager(); //doctrine manager for entity managing
$user = new User();
$user->setEmail($form->email);
if (!$this->checkEmail($form->email)) {
return new JsonResponse(['status' => false, 'message' => 'Email already exists']);
}
$user->setFirstName($form->first_name);
$user->setLastName($form->last_name);
$user->setPassword($form->plain_password);
if (!$form->evaply_id) {
if(!$form->organization_name)
return new JsonResponse(['status' => false, 'message' => 'Missing field Organization']);
$organization = new Organization();
$organization->setName($form->organization_name);
$startGuideStatus['status'] = false;
$startGuideStatus['add_company'] = false;
$startGuideStatus['first_connection'] = false;
$startGuideStatus['first_scorecard'] = false;
$startGuideStatus['first_order'] = false;
$organization->setStartGuideStatus(($startGuideStatus));
$organization->setCustOrSupplier($form->cust_or_supplier);
$evaply_id = $this->generateEvaplyId($form->organization_name);
$organization->setEvaplyId($evaply_id);
$entityManager->persist($organization);
$entityManager->flush();
} else {
$organization = $this->getDoctrine()->getRepository(Organization::class)->findOneBy(array('evaplyId' => $form->evaply_id));
if (!$organization) {
return new JsonResponse(['status' => false, 'message' => 'Invalid Evaply ID']);
}
}
$user->setOrganization($organization);
$user->setUuid($this->generateUUID());
$user->setRoles(['Admin']);
$error = $validator->validate($user);
if (count($error) > 0) {
return new JsonResponse(['status' => false, 'message' => 'Validation error']);
}
if ($form->plain_password != $form->confirm_password) {
return new JsonResponse(['status' => false, 'message' => 'Password miss match']);
}
$user->setPassword(
$userPasswordEncoder->encodePassword(
$user,
$form->plain_password
)
);
$token = $this->generateVerificationToken($user);
$this->logger->info($token);
$verificationUrl = $this->getParameter('app_url') . "account/verify/" . $token;
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$this->sendUserRegisteredMail($user, $verificationUrl, $mailer);
return new JsonResponse(['status' => true, 'message' => "Successfully registered"]);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
return new Response($e->getMessage());
}
}
/**
* Check email duplication in database
*
* #param $email
* #return bool
*/
public function checkEmail($email)
{
$user = $this->getDoctrine()->getRepository(User::class)->findOneBy(array('email' => $email));
if ($user)
return false;
else
return true;
}
/**
* Generate a unique id using organization name and timestamp
*
* #param $orgName
* #return string
*/
public function generateEvaplyId($orgName)
{
$org = strtoupper(substr($orgName, 0, 3));
$dateTime = time();
$evaply_id = $org . $dateTime;
return $evaply_id;
}
/**
* Generate unique uuid for user
* Recursive function
*
* #return int
*/
public function generateUUID()
{
$uuid = rand(1000000000, 9999999999);
$user = $this->getDoctrine()->getRepository(User::class)->findOneBy(array('uuid' => $uuid));
if ($user) {
return $this->generateUUID();
} else {
return $uuid;
}
}
/**
* Send email to user after a successfull registartion.
*
* #param User $user
* #param Swift_Mailer $mailer
* #return int
*/
public function sendUserRegisteredMail(User $user, $verificationUrl, Swift_Mailer $mailer)
{
$message = (new Swift_Message("Successfully Registered"))
->setFrom('admin#evaply.com')
->setTo($user->getEmail())
->setBody(
$this->renderView(
'emails/registration.html.twig',
array('firstName' => $user->getFirstName(), 'lastName' => $user->getLastName(), 'verificationUrl' => $verificationUrl)
),
'text/html'
);
return $mailer->send($message);
}
/**
* Generates a verification token using aes-128-gcm encryption
* encrypts email and uuid
* encode ciphertext with base64 encoder
*
* #param User $user
* #return string
*/
public function generateVerificationToken(User $user)
{
$data = array();
$data['email'] = $user->getEmail();
$data['uuid'] = $user->getUuid();
$plaintext = json_encode($data);
$cipher = $this->getParameter('cipher');
if (in_array($cipher, openssl_get_cipher_methods())) {
$this->logger->info("inside cipher");
//$ivlen = openssl_cipher_iv_length($cipher);
$iv = $this->getParameter('secretIV');
$key = $this->getParameter('cipher_key');
//$tag = $this->getParameter('tagg');
$ciphertext = openssl_encrypt($plaintext, $cipher, $key, $options = 0, $iv);
$original_plaintext = openssl_decrypt($ciphertext, $cipher, $key, $options = 0, $iv);
return $token = base64_encode($ciphertext);
}
}
}
Codes On Tests/Controller Folder
<?php
/**
* Created by PhpStorm.
* User: xavier-phases
* Date: 22/1/19
* Time: 5:09 PM
*/
namespace App\Tests\Controller;
use App\Controller\RegisterController;
use PHPUnit\Framework\TestCase;
class EvaplyTest extends TestCase
{
public function testGenerate_evaply_clid(LoggerInterface $logger)
{
$id= new RegisterController();
$org_name=$id->generateEvaplyId('Tset');
$org = strtoupper(substr($org_name, 0, 3));
$dateTime = time();
$evaply_id = $org . $dateTime;
return $evaply_id;
}
}`
I am calling the generateEvaplyId() method from the register controller. I have installed all test packages. It gives me the following error:
"ArgumentCountError: Too few arguments to function App\Tests\Controller\EvaplyTest::testGenerate_evaply_clid(), 0 passed and exactly 1 expected". KIndly have a look and suggest me a solution.
<?php
namespace App\Tests\Controller;
use App\Controller\RegisterController;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class EvaplyTest extends TestCase
{
/** #var LoggerInterface|MockObject */
protected $logger;
/** #var RegisterController **/
protected $controller;
/**
* {#inheritdoc}
*/
protected function setUp()
{
$this->logger = $this->createMock(LoggerInterface::class);
$this->controller = new RegisterController(
$this->logger,
);
parent::setUp();
}
public function testGenerate_evaply_clid()
{
$result = $this->controller->generateEvaplyId('Test');
$this->assertEquals( 'TEST1548303737',$result);
}
}
Here is how it should be. Initiate controller instance and all it's dependencies in setUp method, then, in all the tests you can reference them. By default, PHPUnit does not expect any dependencies for tests suite, what you tried to do is called data provider (check for data providers docs), it does require additional annotation and used for completely different purpose.
And for the last, I do NOT recommend you to store any part of logic in controllers. Move everything to service layer. Controllers are only to catch request, pass it to manager and return response.

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

Validate user account via mailbox symfony3

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.

Symfony2 custom Authenticator do something when not authenticated

How to manage Full authentication is required to access this resource.?
I want to redirect user when he is not authenticated.
I have custom uthenticater which authenticate user depending on session data, and i want to redirect user when hes not authenticatet.
My authenticator class:
/**
* #Service("sso_authenticator")
*/
class SsoAuthenticator implements SimplePreAuthenticatorInterface
{
/**
* #var SsoUserProvider
*/
protected $userProvider;
/**
* #InjectParams({
* "userProvider" = #Inject("sso_user_provider")
* })
*/
public function __construct(SsoUserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
public function createToken(Request $request, $providerKey)
{
$user = $request->getSession()->get('sso_user');
if (!$user) {
throw new BadCredentialsException('No user found');
}
return new PreAuthenticatedToken(
'anon.', $user, $providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$user = $token->getCredentials();
if (!is_array($user)) {
$user = $token->getUser();
}
if (!$user) {
throw new AuthenticationException('User does not exist.');
}
$ssoUser = $this->userProvider->loadUser($user);
return new PreAuthenticatedToken(
$ssoUser, $user, $providerKey, $ssoUser->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
}
i set the login path to logout path like this:
secured_area:
form_login:
login_path : main_user_logout
then i wrote custom logout handler:
/**
* #Service("sso_authentication_handler")
*/
class SsoAuthenticationHandler implements LogoutSuccessHandlerInterface
{
/**
* #var Router
*/
private $router;
/**
* #var array
*/
protected $ssoUrls;
/**
* #InjectParams({
* "ssoUrls" = #Inject("%wordpress_sso%"),
* "router" = #Inject("router")
* })
*/
public function __construct(array $ssoUrls, Router $router)
{
$this->ssoUrls = $ssoUrls;
$this->router = $router;
}
public function onLogoutSuccess(Request $request)
{
$locale = $request->getLocale();
if ($locale === 'pl') {
$url = $this->ssoUrls[$locale];
} else {
$url = $this->ssoUrls['en'];
}
$url .= '?returnUrl=' . $this->router->generate('main');
return new RedirectResponse($url);
}
}
so with this combination i achive behavior like when youser is not authenticated or when he logout i will redirect him to other site to login, in my example to wordpress.

Categories