I am using Symfony with the FOSUserBundle and now I like to test some things like:
Doctrine lifecycle
Controller behind firewall
For those tests I need to be a specific user or at least in a user group.
How do I mock a user session so that ...
The lifecycle field like "createdAt" will use the logged in user
The Controller act like some mocked user is logged in
Example:
class FooTest extends ... {
function setUp() {
$user = $this->getMock('User', ['getId', 'getName']);
$someWhereGlobal->user = $user;
// after this you should be logged in as a mocked user
// all operations should run using this user.
}
}
You can do this with LiipFunctionalTestBundle. Once you have installed and configured the Bundle, creating and user and log in in tests is easy.
Create a fixture for your user
This creates a user which will be loaded during tests:
<?php
// Filename: DataFixtures/ORM/LoadUserData.php
namespace Acme\MyBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\MyBundle\Entity\User;
class LoadUserData extends AbstractFixture implements FixtureInterface
{
public function load(ObjectManager $manager)
{
$user = new User();
$user
->setId(1)
->setName('foo bar')
->setEmail('foo#bar.com')
->setPassword('12341234')
->setAlgorithm('plaintext')
->setEnabled(true)
->setConfirmationToken(null)
;
$manager->persist($user);
$manager->flush();
// Create a reference for this user.
$this->addReference('user', $user);
}
}
If you want to use groups of users, you can see the official documentation.
Log in as this user in your test
As explained in LiipFunctionalTestBundle's documentation, here is how to load the user in the database and log in as this user:
/**
* Log in as the user defined in the Data Fixture.
*/
public function testWithUserLoggedIn()
{
$fixtures = $this->loadFixtures(array(
'Acme\MyBundle\DataFixtures\ORM\LoadUserData',
));
$repository = $fixtures->getReferenceRepository();
// Get the user from its reference.
$user = $repository->getReference('user')
// You can perform operations on this user.
// ...
// And perform functional tests:
// Create a new Client which will be logged in.
$this->loginAs($user, 'YOUR_FIREWALL_NAME');
$this->client = static::makeClient();
// The user is logged in: do whatever you want.
$path = '/';
$crawler = $this->client->request('GET', $path);
}
What I would do in this case is to create a CustomWebTestCase which extends the Symfony WebTestCase. In the class I would create a method which does the authentication for me.
Here is an example code:
namespace Company\MyBundle\Classes;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\User\User;
abstract class CustomWebTestCase extends WebTestCase
{
/**
* #param array|null $roles
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected static function createAuthenticatedClient(array $roles = null) {
// Assign default user roles if no roles have been passed.
if($roles == null) {
$role = new Role('ROLE_SUPER_ADMIN');
$roles = array($role);
} else {
$tmpRoles = array();
foreach($roles as $role)
{
$role = new Role($role, $role);
$tmpRoles[] = $role;
}
$roles = $tmpRoles;
}
$user = new User('test_super_admin', 'passwd', $roles);
return self::createAuthentication(static::createClient(), $user);
}
private static function createAuthentication(Client $client, User $user) {
// Read below regarding config_test.yml!
$session = $client->getContainer()->get('session');
// Authenticate
$firewall = 'user_area'; // This MUST MATCH the name in your security.firewalls.->user_area<-
$token = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
$session->set('_security_'.$firewall, serialize($token));
$session->save();
// Save authentication
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
return $client;
}
}
The code above will directly create a valid user session and will skip the firewall entirely. Therefore you can create whatever $user you want and it will still be valid. The important part of the code is located in the method createAuthentication. This is what does the authentication magic.
One more thing worth mentioning - make sure you have set framework.session.storage_id to session.storage.mock_file in your config_test.yml so that Symfony will automatically mock sessions instead of you having to deal with that in each test case:
framework:
session:
storage_id: session.storage.mock_file
Now in your test case you would simply extend MyWebTestCase and call the createAuthenticatedClient() method:
class MyTest extends CustomWebTestCase {
public function testSomething() {
//Create authoried and unauthorized clients.
$authenticatedClient = self::createAuthenticatedClient(array("ROLE_SUPER_ADMIN"));
$unauthorizedClient = self::createAuthenticatedClient(array("ROLE_INSUFFICIENT_PERMISSIONS"));
// Check if the page behaves properly when the user doesn't have necessary role(s).
$unauthorizedClient->request('GET', '/secured-page');
$response = $unauthorizedClient->getResponse();
$this->assertFalse($response->isSuccessful());
$this->assertEquals(403, $response->getStatusCode(), "This request should have failed!");
// Check if the page behaves properly when the user HAS the necessary role(s)
$authenticatedClient->request('GET', '/secured-page');
$response = $authenticatedClient->getResponse();
$this->assertTrue($response->isSuccessful());
$this->assertEquals(200, $response->getStatusCode(), "This request should be working!");
}
}
You can see an example in the Symfony official documentation as well.
You can easily do that with LiipFunctionalTestBundle which authorize you lot of shortcut for create Unit Test.
If already you have a form user for create or edit you can use this for your test unit workflow user in your application :
use the makeClient method for logging test
$credentials = array(
'username' => 'a valid username',
'password' => 'a valid password'
);
$client = static::makeClient($credentials);
use your form for test your creation
$crawler = $client->request('GET', '/profile');
$form = $crawler->selectButton('adding')->form();
$form['fos_user_profile_form[firstName]'] = 'Toto';
$form['fos_user_profile_form[lastName]'] = 'Tata';
$form['fos_user_profile_form[username]'] = 'dfgdgdgdgf';
$form['fos_user_profile_form[email]'] = 'testfgdf#grgreger.fr';
$form['fos_user_profile_form[current_password]'] = 'gfgfgdgpk5dfgddf';
testing "createdAt" with just call findOneBy in repository user like this
$user = $this->getObjectManager()
->getRepository('AcmeSecurityBundle:User')
->findOneBy(array('username' => 'testCreateUserUsername'));
$this->assertTrue($user->getCreatedAt() == now());
Related
In Symfony 4.4 I am attempting simulate authenticating a user so that I can write PHPUnit tests for a secured area of my application. I am using the ZenstruckFoundry to create a factory for my User that will be authenticated.
I have also followed the Symfony docs for Creating an Authentication Token and originally opened an issue in the zenstruck/foundry repo. Since this doesn't actually seem to be an issue with Foundry I'm asking here on SO.
My test to an endpoint behind my main firewall simply looks like this:
/**
* #test
*/
public function it_displays_the_dashboard()
{
$user = UserFactory::new()->createOne();
$this->auth($user);
$this->appClient->request('GET', '/'); // <- this requires an auth'd user
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('[data-test="user-profile"]', $user->getUsername());
}
/**
* Simulate authentication
*
* #param Proxy $user
*/
protected function auth(Proxy $user)
{
$session = self::$container->get('session');
$firewallName = 'main';
// **********************************************
// **********************************************
// $user = $user->object(); <-- If I uncomment to use the actual User model then auth doesn't work
// **********************************************
// **********************************************
$token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles());
$session->set('_security_'.$firewallName, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->appClient->getCookieJar()->set($cookie);
}
The issue I'm encountering is this: If I pass the Proxy user to UsernamePasswordToken then the token is created successfully and the user is authenticated. However, this causes Twig to not be able to use its magic methods to resolve getter attributes. For example, my Twig template has {{ user.username }}, Behind the scenes Twig resolves this to the getUsername() method on my User instance. But since the Proxy class doesn't have such a method, I get this error:
Error: Call to undefined method App\Model\User::username()
If I modify my template to be {{ user.getUsername }} then my test passes.
But, if I pass the User class directly to UsernamePasswordToken using $user->object() then the token is initially created, but then seemingly doesn't exist by the time authentication takes place and my test fails because I'm not authenticated.
If I look at the stacktrace I can see that the User is no longer the User that was used when creating the token and I see the following:
/Users/myuser/code/adminapp/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:84:
class Symfony\Component\Security\Core\Authentication\Token\AnonymousToken#847 (6) {
private $secret =>
string(7) "OxIzLMV"
private $user =>
string(5) "anon."
private $roles =>
array(0) {
}
private $roleNames =>
array(0) {
}
private $authenticated =>
bool(true)
private $attributes =>
array(0) {
}
}
I would like to be able to pass the actual User class using $user->object() so that I don't have to manipulate my Twig templates by using the getters directly.
I followed this tutorial to setup Oauth2 login with Auth0 in Symfony.
How can I access the email address of the logged in Auth0 user?
Notes:
The login works successfully (oauth2 via google on the auth0 side, then redirected back)
$this->getUser() from the controller shows the correct username
scopes configured in hwi_oauth.yaml: openid profile email
The Auth0 record (on their admin dashboard) contains email addresses for the users
The bottom of the article references OAuthUserProvider to get user data but I've loaded the service and get only the username again
My code is the same as the article I referenced.
This is the controller I need access to the email address in, followed by the output of the dd($this->getUser().
class UserController extends AbstractController
{
/**
* #Route("/user", name="user")
*/
public function index()
{
dd($this->getUser());
return $this->render('user/index.html.twig', [
'controller_name' => 'UserController',
]);
}
}
^ HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser {#580 ▼
#username: "coder1" }
You have to extend your User entity with HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser Class, and extend the HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider Class with your own.
Then register it as a service and use that in your firewall settings. Here is an article walking through it:
https://inchoo.net/dev-talk/symfony-hwioauthbundle-and-google-sign-in/
In your OAuthUserProvider class you can modify the loadUserByOAuthUserResponse and load your user from database.
Here are the important code pieces:
update your firewall
oauth_user_provider:
service: ib_user.oauth_user_provider
add the services
hwi_oauth.user.provider.entity:
class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider
ib_user.oauth_user_provider:
class: Foggyline\Bundle\TickerBundle\Auth\OAuthProvider
arguments: [#session, #doctrine, #service_container]
Here is the OAuthProvider class I'm using:
<?php
namespace App\Auth;
use App\Repository\UserRepository;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use App\Entity\User;
class OAuthProvider extends OAuthUserProvider
{
protected $session, $doctrine, $admins, $userRepository;
public function __construct($session, $doctrine, $service_container, UserRepository $userRepository)
{
$this->session = $session;
$this->doctrine = $doctrine;
$this->container = $service_container;
$this->userRepository = $userRepository;
}
public function loadUserByUsername($username)
{
$result = $this->userRepository->findBy(['name' => $username]);
if (count($result)) {
return $result[0];
} else {
return new User($username);
}
}
public function loadUserByEmail($email)
{
$result = $this->userRepository->findBy(['email' => $email]);
if (count($result)) {
return $result[0];
} else {
return new User($email);
}
}
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
//Data from response
$email = $response->getEmail();
$nickname = $response->getNickname();
$realname = $response->getRealName();
$avatar = $response->getProfilePicture();
//set data in session
$this->session->set('email', $email);
$this->session->set('nickname', $nickname);
$this->session->set('realname', $realname);
$this->session->set('avatar', $avatar);
$result = $this->userRepository->findBy(['email' => $email]);
if (!count($result)) {
$user = new User($email);
$user->setName($realname);
$user->setEmail($email);
$user->setRoles(['ROLE_USER']);
$factory = $this->container->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword(md5(uniqid()), $user->getSalt());
$user->setPassword($password);
} else {
$user = $result[0];
$user->setUsername($realname);
}
$em = $this->doctrine->getManager();
$em->persist($user);
$em->flush();
//set id
$this->session->set('id', $user->getId());
return $this->loadUserByEmail($response->getEmail());
}
}
With $this->getUser() you are getting UserInterface object from security token of currently logged in user. So you have access to all the regular methods of your User entity. You can do like this:
$currentUser = $this->getUser()->getEmail();
I need to check if the user has accepted the latest privacy policy, before executing any controller. Something like this:
if($user->getAcceptedTnc() < 2) // unless I'm in some specific routes...
{
return $this->render('app/privacyPolicy.html.twig');
// or alternatively do AppController::privacyPolicyAction()
}
Where can this be done?
I've thought at logging out all the users and putting this in some authentication listener...
I solved with a onKernelController listener, so I'm doing the check at every page load:
public function onKernelController(FilterControllerEvent $event)
{
// [ return on some conditions (ajax calls, specific controller/routes, ...) ]
/** #var User $user */
$user = $this->token ? $this->token->getUser() : null;
if($user && $user !== 'anon.' && $user->getLastAcceptedTerms() < Utils::CURRENT_TERMS_VERSION) {
// [ return if the user is non-EU, has specific roles, etc...]
// The user must accept the new Terms. Show him AppController:privacyPolicyAction
$request = new Request();
$request->attributes->set('_controller', 'App\Controller\AppController:privacyPolicyAction');
$event->setController($this->controllerResolver->getController($request));
}
}
I am developing an application using Symfony2 and doctrine 2. I would like to know how can I get the currently logged in user's Id.
Current Symfony versions (Symfony 4, Symfony >=3.2)
Since Symfony >=3.2 you can simply expect a UserInterface implementation to be injected to your controller action directly. You can then call getId() to retrieve user's identifier:
class DefaultController extends Controller
{
// when the user is mandatory (e.g. behind a firewall)
public function fooAction(UserInterface $user)
{
$userId = $user->getId();
}
// when the user is optional (e.g. can be anonymous)
public function barAction(UserInterface $user = null)
{
$userId = null !== $user ? $user->getId() : null;
}
}
You can still use the security token storage as in all Symfony versions since 2.6. For example, in your controller:
$user = $this->get('security.token_storage')->getToken()->getUser();
Note that the Controller::getUser() shortcut mentioned in the next part of this answer is no longer encouraged.
Legacy Symfony versions
The easiest way to access the user used to be to extend the base controller, and use the shortcut getUser() method:
$user = $this->getUser();
Since Symfony 2.6 you can retrieve a user from the security token storage:
$user = $this->get('security.token_storage')->getToken()->getUser();
Before Symfony 2.6, the token was accessible from the security context service instead:
$user = $this->get('security.context')->getToken()->getUser();
Note that the security context service is deprecated in Symfony 2 and was removed in Symfony 3.0.
In symfony2, we can get this simpler by this code:
$id = $this->getUser()->getId();
You can get the variable with the code below:
$userId = $this->get('security.context')->getToken()->getUser()->getId();
This can get dressed in the method:
/**
* Get user id
* #return integer $userId
*/
protected function getUserId()
{
$user = $this->get('security.context')->getToken()->getUser();
$userId = $user->getId();
return $userId;
}
And induction $this->getUserId()
public function example()
{
print_r($this->getUserId());
}
I'm working on a Symfony 2 application where the user must select a profile during the login process.
Users may have multiples profiles to work with and they only know their own profiles. So first, I need to prompt for username and password, if those are correct, I should not login the user, I need to prompt for profile witch user will use during the session.
So, I show a form with a username and password field, and send it using an Ajax request, that request responds with the profile list if username and password are correct or an error code otherwise. Finally the user logs into the system using username, password and profile.
The problem is that I don't know how to check if authentication data is correct (using all my authentication managers, users providers, etc) to accomplish this intermediate step (prompts for profile) without in fact logging the user.
Can anyone help me with this?
A problem with #Jordon's code is that it will not work with hashing algorithms that generate different hashes for the same password (such as bcrypt that stories internally its parameters, both the number of iterations and the salt). It is more correct to use isPasswordValid of the Encoder for comparing passwords.
Here is the improved code that works fine with bcrypt:
$username = trim($this->getRequest()->query->get('username'));
$password = trim($this->getRequest()->query->get('password'));
$em = $this->get('doctrine')->getManager();
$query = $em->createQuery("SELECT u FROM \Some\Bundle\Entity\User u WHERE u.username = :username");
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
if ($user) {
// Get the encoder for the users password
$encoder_service = $this->get('security.encoder_factory');
$encoder = $encoder_service->getEncoder($user);
// Note the difference
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
// Get profile list
} else {
// Password bad
}
} else {
// Username bad
}
You could do something like this to retrieve the user and manually test the password -
$username = trim($this->getRequest()->query->get('username'));
$password = trim($this->getRequest()->query->get('password'));
$em = $this->get('doctrine')->getEntityManager();
$query = $em->createQuery("SELECT u FROM \Some\Bundle\Entity\User u WHERE u.username = :username");
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
if ($user) {
// Get the encoder for the users password
$encoder_service = $this->get('security.encoder_factory');
$encoder = $encoder_service->getEncoder($user);
$encoded_pass = $encoder->encodePassword($password, $user->getSalt());
if ($user->getPassword() == $encoded_pass) {
// Get profile list
} else {
// Password bad
}
} else {
// Username bad
}
Once you've got your profile back from the client, you can perform the login manually in the AJAX server controller easily enough too -
// Get the security firewall name, login
$providerKey = $this->container->getParameter('fos_user.firewall_name');
$token = new UsernamePasswordToken($user, $password, $providerKey, $user->getRoles());
$this->get("security.context")->setToken($token);
// Fire the login event
$event = new InteractiveLoginEvent($this->getRequest(), $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
Might need a few use lines -
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
I used the code from #Jordon and #Potor Polak to wrap the logic in a standalone service that used the current access token to validate the password. Maybe some needs this:
services.yml:
app.validator.manual_password:
class: AppBundle\Service\ManualPasswordValidator
arguments:
- '#security.token_storage'
- '#security.encoder_factory'
ManualPasswordValidator.php:
<?php
namespace AppBundle\Service;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
/**
* Class ManualPasswordValidator
*
* #package AppBundle\Service
*/
class ManualPasswordValidator
{
/**
* #var EncoderFactory
*/
protected $encoderFactory;
/**
* #var TokenStorage
*/
protected $tokenStorage;
/**
* ManualPasswordValidator constructor.
*
* #param EncoderFactory $encoderFactory
* #param TokenStorage $tokenStorage
*/
public function __construct(TokenStorage $tokenStorage, EncoderFactory $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
$this->tokenStorage = $tokenStorage;
}
/**
* #param $password
* #return bool
*/
public function passwordIsValidForCurrentUser($password)
{
$token = $this->tokenStorage->getToken();
if ($token) {
$user = $token->getUser();
if ($user) {
$encoder = $this->encoderFactory->getEncoder($user);
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return true;
}
}
}
return false;
}
}
After this you can inject the ManualPasswordValidator wherever you want and use it like:
$password = $request->get('password');
$passwordIsValid = $this->manualPasswordValidator->passwordIsValidForCurrentUser($password);
The only way I could authenticate my users on a controller is by making a subrequest and then redirecting. Here is my code, I'm using silex but you can easily adapt it to symfony2:
$subRequest = Request::create($app['url_generator']->generate('login_check'), 'POST', array('_username' => $email, '_password' => $password, $request->cookies->all(), array(), $request->server->all());
$response = $app->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, false);
return $app->redirect($app['url_generator']->generate('curriculos.editar'));
In Symfony 4, the usage of the UserPasswordEncoderInterface is recommended in Controllers. Simply add a UserPasswordEncoderInterface as a parameter to the function in which you want to check the password and then add the code below.
public function changePasswordAction($old, $new, UserPasswordEncoderInterface $enc) {
// Fetch logged in user object, can also be done differently.
$auth_checker = $this->get('security.authorization_checker');
$token = $this->get('security.token_storage')->getToken();
$user = $token->getUser();
// Check for valid password
$valid = $encoder->isPasswordValid($user, $old);
// Do something, e.g. change the Password
if($valid)
$user->setPassword($encoder->encodePassword($user, $new));
}
Symfony 5.4
Password validation can be done using UserPasswordHasherInterface
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AuthenticaitonServices
{
public function __construct(UserPasswordHasherInterface $hasher)
{
$this->hasher = $hasher;
}
public function validate($request)
{
$form = [
"username" => $request->request->get("_username"),
"password" => $request->request->get("_password")
];
if(!$this->hasher->isPasswordValid($user, $form['password']))
{
// Incorrect Password
} else {
// Correct Password
}
isPasswordValid returns a bool response
If anyone checking solution for password validation in Symfony 5.4.
Above code is for validating password posted from a login form.
Hope this is helpful.