FOSUserBundle Unit testing - symfony

I am working on setting up a site with fosUserBundle. I need to write some unit tests, but I cannot seems to get authentication to work correctly for use with them. I have have tried a few methods with no luck.
approach1 - chain providers in security settings to make a standard login I can rely on. When I do this the only authentication that works is the in_memory provider meaning I can login in with the admin user but not any users created by fosUserBundle
security.yml
security:
encoders:
"FOS\UserBundle\Model\UserInterface": plaintext
"Symfony\Component\Security\Core\User\User": plaintext
providers:
chain_provider:
providers: [in_memory, fos_userbundle]
in_memory:
users:
admin: { password: admin, roles: [ 'ROLE_ADMIN' ] }
fos_userbundle:
id: fos_user.user_manager
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
login_path: /
csrf_provider: form.csrf_provider
default_target_path: /media
logout: true
anonymous: true
http_basic:
provider: in_memory
Method 2- I created a user in my setup of the unit test. But with this method I can never login with the user I just created, I am not requiring user confirmation.
public function setUp() {
$kernel = static::createKernel();
$this->repo = $kernel->boot();
$this->repo = $kernel->getContainer();
$userManager = $this->repo->get('fos_user.user_manager');
$email = "testing".(string) self::newRandomNumber()."#test.com";
$un = "test".(string) self::newRandomNumber();
$fn = "testin";
$ln = "laster";
$pwd = "passworD1";
$user = $userManager->createUser();
$user->setEmail($email);
$user->setUsername($un);
$user->setFirstName($fn);
$user->setLastName($ln);
$user->setPlainPassword($pwd);
$user->setBimStaff(True);
// Persist the user to the database
$userManager->updateUser($user);
$this->user = $user;
$this->client = static::createClient();
$crawler = $this->client->request('GET', '/');
$form = $crawler->selectButton('Login')->form();
// set some values
$un = 'Lucas'.self::newRandomNumber();
$form['_username'] = $un;
$form['_password'] = $pwd;
// submit the form
$crawler = $this->client->submit($form);
print_r($this->client->getResponse());
$this->client->followRedirects();
}
I have tried both login in at the form and http basic authentication but neither seem to work.
Any one have any advice?
Thanks,
Cory

Hmm, It seems that if you chain providers in the security you cannot use just one of the providers, at least that solved the issue for me.
// security.yml
providers:
chain_provider:
providers: [in_memory, fos_userbundle]
in_memory:
users:
admin: { password: admin, roles: [ 'ROLE_ADMIN' ] }
fos_userbundle:
id: fos_user.user_manager
firewalls:
main:
pattern: ^/
form_login:
provider: chain_provider
login_path: /
csrf_provider: form.csrf_provider
default_target_path: /media
logout: true
anonymous: true
http_basic:
provider: chain_provider
FOr some reason as soon as I changed both providers to chain_provider it started cooperating.
My unit test now looks like this
public function setUp() {
$kernel = static::createKernel();
$this->repo = $kernel->boot();
$this->repo = $kernel->getContainer();
$this->client = static::createClient(array(), array(
'PHP_AUTH_USER' => 'admin',
'PHP_AUTH_PW' => 'admin',
));
$this->client->followRedirects();
}
If copying this I would suggest using stronger passwords for your users :)

I had the same problem but I wanted option 2 to work. Your example was almost there and helped me. I think the reason it wasn't working for you is that the user wasn't enabled. The user constructor in FOSUserBundle will create disabled users by default, so you must call $user->setEnable(true)
Here's my working example
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
class MyControllerTest extends WebTestCase
{
private $client = null;
public function setUp()
{
$this->client = static::createClient();
}
private function logIn()
{
$session = $this->client->getContainer()->get('session');
$userManager = $this->client->getContainer()->get('fos_user.user_manager');
$user=$userManager->findUserByUsername('tester');
if (!$user) {
$user = $userManager->createUser();
$user->setEmail('test#example.com');
$user->setUsername('tester');
$user->setPlainPassword('foo');
$user->setEnabled(true);
$user->addRole('ROLE_SUPER_ADMIN');
$userManager->updateUser($user);
}
$firewall = 'main_firewall';
$token = new UsernamePasswordToken($user, null, $firewall, array('ROLE_SUPER_ADMIN'));
$session->set('_security_'.$firewall, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
public function testCompleteScenario()
{
$this->logIn();
$crawler = $this->client->request('GET', '/foo/');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(),
"Unexpected HTTP status code for GET /foo/".$this->client->getResponse()->getContent());
}
}

Related

Symfony 4, PHPUnit - authorize user for unit test

I'm writing test to do the unit testing of controller. I want to check if I get the proper content for the url, which requires user to be logged in.
I have following test written
class AdminControllerTest extends WebTestCase
{
private $client = null;
public function setUp()
{
$this->client = static::createClient();
}
public function testAdminHome(): void
{
$this->logIn();
$crawler = $this->client->request('GET', '/admin/');
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$this->assertGreaterThan(0, $crawler->filter('html:contains("Welcome to Admin Panel")')->count());
}
private function logIn()
{
$user = new User();
$user->setId(1);
$user->setEmail('test#admin.com');
$user->setFullName('Admin User');
$user->setEnabled(true);
$session = $this->client->getContainer()->get('session');
$firewallName = 'main';
$firewallContext = 'main';
$token = new UsernamePasswordToken($user, null, $firewallName, ['ROLE_ADMIN']);
$session->set('_security_'.$firewallContext, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
}
Configuration in my security.yml
security:
encoders:
App\Entity\User: auto
providers:
database_users:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
pattern: ^/
context: main
form_login:
check_path: security_login
login_path: security_login
csrf_token_generator: security.csrf.token_manager
default_target_path: admin_home
logout:
path: security_logout
target: security_login
access_control:
- { path: ^/admin, roles: [ROLE_ADMIN, ROLE_EDITOR] }
role_hierarchy:
ROLE_ADMIN: ROLE_EDITOR
ROLE_EDITOR: ROLE_USER
However assertion for the Response code returns me 200, but I land on login page instead of admin home. Also there is not required content, as I'm on login page not an admin home.
Could you please check the code and help me find out the issue?
User #isom had right, I should have the user object persisted, so I have used fixtures to have a user persisted and then everything is fine.
Instead of creating new user object I pull it from DB with following code:
$user = self::$kernel->getContainer()->get('doctrine')->getRepository(User::class)->find(1);

FOSUserBundle - PHPUnit - Mock a user - No User Provider

I am using Symfony 3.2 with the FOSUserBundle and I am trying to write functional tests for function that require authentication in specific roles.
I used the approach posted by #tftd here, but when I run a phpunit test, I get a 500 error: There is no user provider for user "Symfony\Component\Security\Core\User\User".
My webTestCase class looks like this:
abstract class CustomWebTestCase extends WebTestCase
{
/**
* #param array|null $roles
* #return \Symfony\Bundle\FrameworkBundle\Client
*
* https://stackoverflow.com/questions/35565196/fosuserbundle-phpunit-mock-a-user
*/
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);
$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 = 'main'; // 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;
}
And my test routine looks like this:
class TryoutAdminControllerTest extends CustomWebTestCase
{
public function testTryoutListAction()
{
$authenticatedClient = self::createAuthenticatedClient(array('ROLE_USER'));
$crawler = $authenticatedClient->request('GET', '/admin/tryout');
$this->assertEquals(302, $authenticatedClient->getResponse()->getStatusCode(), 'No access allowed!');
$authorizedClient = self::createAuthenticatedClient(array('ROLE_ADMIN'));
$crawler = $authorizedClient->request('GET', '/admin/tryout');
$this->assertEquals(200, $authorizedClient->getResponse()->getStatusCode(), 'Access granted!');
}
}
security.yml:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
switch_user: true
remember_me:
secret: '%secret%'
lifetime: 604800 # 1 week in seconds
path: /
domain: ~
user_provider: fos_userbundle
And finally config_test.yml
imports:
- { resource: config_dev.yml }
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
web_profiler:
toolbar: false
intercept_redirects: false
swiftmailer:
disable_delivery: true
If someone could let me know what I'm missing, I'd appreciate it!
You probably need to instantiate your own User class that extends from FOS\UserBundle\Model\User instead of Symfony\Component\Security\Core\User\User in your WebTestCase since you are using the FosUserBundle user provider.
Consider using a test database and data fixtures as you will probably need it for your functional tests. The LiipFunctionalTestBundle may help.
Keep in mind that nothing is being mocked in this test, as suggested in the question title. Mocks must be used in unit tests, not in functional tests.

User object is not accessable in the controller in symfony

I'm trying to use auto login feature in symfony something like firing the login event and setting the user object. The user object is available in that controller but when I try to use other controller the user object says annon instead of showing the logged in user info
Controller A
private function autoLogin($request, $username)
{
$em = $this->getDoctrine()->getManager();
if (!$usr = $em->getRepository('AppBundle:User')->findOneBy(['username' => $username])) {
throw $this->createNotFoundException('User does not exist');
}
$token = new UsernamePasswordToken($usr, $usr->getPassword(), "secured_area", $usr->getRoles());
$this->get('security.token_storage')->setToken($token);
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $loginEvent);
$user = $this->get('security.token_storage')->getToken()->getUser();
dump($user); // can see user object without any issue
if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
return $usr;
}
Controller B
public function editAction(Request $request)
{
$user = $this->get('security.token_storage')->getToken()->getUser();
print_r($user); // result is annon.
}
security.yml
security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
providers:
doctrine_provider:
entity:
class: AppBundle:User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/
form_login:
login_path: security_login
check_path: security_login
csrf_token_generator: security.csrf.token_manager
logout:
path: /logout
target: /login
access_control:
- { path: ^/.*, roles: IS_AUTHENTICATED_ANONYMOUSLY }
I'd assume that you're not using any security setup and the second controller is called after user refreshes the page.
The most likely problem is that your user is not persisted into the session. Symfony\Component\Security\Http\Firewall\ContextListener is responsible for that. If you have a look at onKernelResponse() method you can find out how it does it. Basically it gets token from token storage, serialize it and stores to the session. On the request it does opposite: gets token from session and puts it to token storage.
I'd suggest that you play with the configuration of firewall and set up something like this:
firewalls:
autologin:
pattern: /autologinUrl/
context: autologing
In this case context listener will be called doing session-related stuff and your code should work.

Combine a stateful application with stateless JWT

We have a FOSUserBundle login system authenticating via LDAP and the fr3d LDAP bundle. It behaves like a normal multiple page application using sessions. We also have several RESTful endpoints using the FOSRestbundle and normal sessions for authentication. However, we need to share a few end points with an external application.
We managed to implement JWT using the Lexik bundle. It returns a token just fine. However, I don't know the best way to let a user using our login form to get this token so their request can pass it along in the header or session. My question is how to allow a user to login to our application in a stateful manner, but also receive the JWT and pass it to the server on ajax requests. This way I can allow external clients to connect directly to the API. Below is my symfony2 security configuration, security.yml:
security:
#erase_credentials: false
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
chain_provider:
chain:
providers: [my_user_provider, fr3d_ldapbundle]
in_memory:
memory:
users:
admin: { password: secret, roles: 'ROLE_ADMIN' }
my_user_provider:
id: app.custom_user_provider
fos_userbundle:
id: fos_user.user_provider.username
fr3d_ldapbundle:
id: fr3d_ldap.security.user.provider
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: IS_AUTHENTICATED_FULLY }
- { path: ^/api, role: IS_AUTHENTICATED_FULLY }
- { path: ^/api/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/api/login
fr3d_ldap: ~
provider: chain_provider
anonymous: true
stateless: false
form_login:
check_path: /api/login_check
username_parameter: username
password_parameter: password
require_previous_session: false
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
provider: chain_provider
stateless: false
lexik_jwt:
throw_exceptions: true
create_entry_point: true
main:
pattern: ^/
fr3d_ldap: ~
form_login:
# provider: fos_userbundle
provider: chain_provider
always_use_default_target_path: true
default_target_path: /
csrf_provider: security.csrf.token_manager
logout: true
anonymous: true
switch_user: { role: ROLE_LIMS-BIOINFO}
EDIT:
Based on Kévin's answer I decided to implement a custom Twig extension to get the token for the logged in user on each page load:
AppBundle/Extension/JsonWebToken.php:
<?php
namespace AppBundle\Extension;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class JsonWebToken extends \Twig_Extension
{
/**
* #var ContainerInterface
*/
private $container;
/**
* #var JWTManagerInterface
*/
private $jwt;
public function __construct(ContainerInterface $container, JWTManagerInterface $jwt)
{
$this->container = $container;
$this->jwt = $jwt;
}
public function getName()
{
return 'json_web_token';
}
public function getFunctions()
{
return [
'json_web_token' => new \Twig_Function_Method($this, 'getToken')
];
}
public function getToken()
{
$user = $this->container->get('security.token_storage')->getToken()->getUser();
$token = $this->jwt->create($user);
return $token;
}
}
app/config/services.yml:
app.twig_jwt:
class: AppBundle\Extension\JsonWebToken
arguments: ["#service_container", "#lexik_jwt_authentication.jwt_manager"]
tags:
- { name: twig.extension }
app/Resources/views/layout.html.twig
<script>window.jsonWebToken = '{{ json_web_token() }}';</script>
app/Resources/modules/layout/app.js:
var jsonWebToken = window.jsonWebToken;
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization","Bearer " + jsonWebToken);
}
});
So far this seems to be working well. It let's my external API users and internal application users share the same authentication methods.
As the JWT token must be stored client-side (and not in a cookie to prevent CSRF attacks), you can use the create method of the lexik_jwt_authentication.jwt_manager service provided by LexikJWTAuthenticationBundle to generate a token after the login, then inject this token in a <script> tag in the generated HTML.
Hey i recently came across this same situation.
To generate the JWT I created a redirect listener
class RedirectListener implements EventSubscriberInterface
{
//the private variables go up here
public function __construct(\Twig_Environment $twig, TokenStorageInterface $sam, EntityManagerInterface $em, JWTTokenManagerInterface $JWTTokenManager)
{
$this->twig = $twig;
$this->sam = $sam;
$this->em = $em;
$this->JWTTokenManager = $JWTTokenManager;
}
public function kernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$route = $request->get('_route');
$routeParams = $request->get('_route_params');
$pathInfo = $request->getPathInfo();
$matchApp = preg_match('/\/app/', $pathInfo);
if ($event->isMasterRequest()) {
if ($matchApp) {
$token = $this->sam->getToken();
if ($token) {
/** #var User $user */
$user = $token->getUser();
if($user instanceof User){
$token = $this->JWTTokenManager->create($user);
$this->twig->addGlobal('jwt', $token);
}
}
}
}
return $event;
}
}
This helped me get the JWT to my twig template (I used my base template to make sure it's present on every page)
{% if jwt is defined %}
<span class="hidden" id="jwt" data-jwt="{{ jwt ? jwt : 'null' }}"></span>
{% endif %}
Now using autobahn JS I can subscribe using the JWT :
let jwt = $('#jwt').data('jwt');
let connection = new Connection({url:"ws://127.0.0.1:8080/ws", realm:"realm1"});
connection.onopen = (session) => {
function onevent(args) {
console.log("Event:", args[0])
}
session.subscribe(jwt, onevent);
}
connection.open();
Next the server can now receive messages with the JWT from the JS
public function __construct(ContainerInterface $container)
{
$router = new Router();
$realm = "realm1";
$router->addInternalClient(new Pusher($realm, $router->getLoop()));
$router->addTransportProvider(new RatchetTransportProvider("0.0.0.0", 8080));
try{
$router->start();
} catch (\Exception $exception){
var_dump($exception->getMessage());
}
}
I now need to register a module for the router that will act as a listener and send messages back to the registered topic (JWT).
I'm not 100% there yet so any advice would be appreciated and I'll keep this updated as I go along.

symfony2 Bad credentials at login

I've been trying to get a database login system working with Symfony2. I've read through the docs but I'm totally stumped now as to what to do...
Every time I try to log in I just get 'Bad Credentials'.
security.yml
security:
encoders:
D\UserBundle\Entity\User:
algorithm: sha512
encode_as_base64: false
iterations: 1
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
providers:
user_db:
entity: { class: DUserBundle:User}
firewalls:
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: /login
check_path: /login_check
logout:
path: /logout
target:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/user, roles: ROLE_USER }
UserRepository.php
<?php
namespace D\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$q = $this
->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf('Unable to find an active admin AcmeUserBundle:User object identified by "%s".', $username), null, 0, $e);
}
return $user;
}
public function refreshUser(UserInterface $user)
{
$class = get_class($user);
if (!$this->supportsClass($class)) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $class));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $this->getEntityName() === $class || is_subclass_of($class, $this->getEntityName());
}
}
User.php
UserController.php
that's my database, table(users)
id:5
username:Pawel
salt:5633267d072cd3119af5270d64b4b45d
password:test
email:test#test.pl
is_active1
Your security.yml should look like this
providers:
user_db:
entity: { class: DUserBundle:User, property: username }
Yo do not need the UserRepository class at all.
Really late sorry but as the question is not answered yet...
I am not sure if you can use the alias of your entity as the "class" value for your provider.
Maybe try to set it up with the complete namespaced entity name:
volunteers:
entity:
class: D\UserBundle\Entity\User
But I think the main problem is that your password should - at least, and probably HAVE TO - be encrypted in your database.
If you want to use the security encoder that you define in your security.yml config file,
the right way - according to the documentation - is to manage it after your registration form validation.
Let's imagine you have a registration form for your users :
# Any routing file
d_userbundle_register:
pattern: /register
defaults:
_controller: DUserBundle:Users:register
requirements:
_method: get
d_userbundle_register_check:
pattern: /register_check
defaults:
_controller: DUserBundle:Users:registerCheck
requirements:
_method: post
And the registerCheckAction of your controller:
// UsersController.php
public function registerCheckAction()
{
$request = $this->getRequest();
$user = new D\UserBundle\Entity\User();
$registrationForm = $this->createForm(new D\UserBundle\Form\UserType(), $user);
$registrationForm->bind($request);
if ($registrationForm->isValid()) {
$em = $this->getDoctrine()->getManager(); // use getEntityManager() instead of getManager() for old symfony2 versions
// Get the factory encoder service
$encoderFactory = $this->get('security_encoder.factory');
// Retrieve the algorithms, iterations and everything defined in your security.yml file
$encoder = $encoderFactory->getEncoder($user);
// Encrypt the password with the user's plaintext password AND the user salt
$encryptedPassword = $encoder->encodePassword($user->getPassword(), $user->getSalt());
// Then set the user's encrypted password :)
$user->setPassword($encryptedPassword);
$em->persist($user);
$em->flush();
// Thanks the user
$request->getSession()->getFlashBag()->add('success', 'Heyoh, welcome on board !');
return $this->redirect($this->generateUrl('d_userbundle_login'));
}
return $this->render('DUserBundle:User:registration.html.twig', array(
'registration_form' => $registrationForm->createView()
));
}

Resources