authorizationChecker->isGranted in a phpUnit test - symfony

I'm currently creating my unit test for my application but it's the first time I do it.
I want to test this function :
/**
* This method search if the user is already in a team for the same tournament than the one passed in argument
* #param User $user
* #param Team $team
* #return bool|Team|mixed
*/
public function isAlreadyApplicant($user, Team $team) {
if (!$user || !$this->authorizationChecker->isGranted("ROLE_USER")) {
return false;
}
foreach ($user->getApplications() as $userTeam) {
/** #var Team $userTeam */
if ($userTeam->getTournament()->getId() === $team->getTournament()->getId()) {
return $userTeam;
}
}
foreach ($user->getTeams() as $userTeam) {
/** #var Team $userTeam */
if ($userTeam->getTournament()->getId() === $team->getTournament()->getId()) {
return $userTeam;
}
}
foreach ($user->getManagedTeam() as $userTeam) {
/** #var Team $userTeam */
if ($userTeam->getTournament()->getId() === $team->getTournament()->getId()) {
return $userTeam;
}
}
return false;
}
As you can see, the first test is to check if the user have the ROLE_USER.
When I try to "log my user", I have this message :
Fatal error: Call to a member function getToken() on null in \vendor\symfony\symfony\src\Symfony\Component\Security\Core\Authorization\AuthorizationChecker.php on line 56
I tried what I found in the Symfony doc but I must miss something. This is my test Class:
class ApplicationCheckerTest extends WebTestCase
{
protected $client = null;
public function setUp() {
$this->client = static::createClient();
/** #var User $user */
$user = $this->client->getContainer()->get('doctrine')->getManager()->getRepository('MGDUserBundle:User')->findOneBy(array("email" => 'Player11.Player11#test.com'));
$this->loginUser("main", $user);
}
protected function loginUser($firewallName, UserInterface $user, array $options = array(), array $server = array())
{
$this->client = static::createClient();
$token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles());
static::$kernel->getContainer()->get('security.token_storage')->setToken($token);
$session = $this->client->getContainer()->get('session');
$session->set('_security_'.$firewallName, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
public function testIsAlreadyApplicantIsNotConnected()
{
$user = new User();
$team = new Team();
$router = $this->createMock(Router::class);
$authorizationChecker = $this->createMock(AuthorizationChecker::class);
$applicationChecker = new ApplicationChecker($router, $authorizationChecker);
$applicationChecker->isAlreadyApplicant($user, $team);
}
}
And my security.yml looks like :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
provider: main
form_login:
login_path: fos_user_security_login
check_path: fos_user_security_check
logout:
path: fos_user_security_logout
target: /
remember_me:
secret: '%secret%'
I try to connect a user I created with fixtures.
I'm not sure about the way I try to do it, maybe I'm on the wrong path, don't hesitate to correct me if I'm wrong!
For information, I'm in Symfony 3.2.13
Have a good day

The WebTestCase and the example from the Symfony documentation that you're using should be used for a functional test of your controller.
The way you have set up your authentication for the client is correct. But you are not using that client to make a request to the bootstrapped Symfony kernel in your test case. You are just making a simple unit test on a manually created instance of your service, which is fine.
You can simply use the PHPUnit\Framework\TestCase for that.
It is possible to use the WebTestCase to test your service and overwrite the TokenStorage which is used in the AuthorizationChecker in the kernel container instead of the client container, and then also fetch your service from the kernel container instead of instantiating it yourself. But I don't see much benefit in it. There is no need to test the Symfony component (if isGranted is working). That is in the scope of the Symfony project and most likely already covered there.
Your error
The reason for your error is, that a final method can't be mocked. As a workaround you can set the dependency in your ApplicationChecker constructor to AuthorizationCheckerInterface and then create a mock from the interface in your test.
$authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class);
$authorizationChecker->method('isGranted')->willReturn(true);

Related

How to implement Symfony 5 Authentication without Doctrine?

Am in the process of rewriting an existing PHP project to Symfony 5.3. I want to upgrade its authentication system to Symfony's. Only issue: Doctrine is not an option in this project.
How can I use Symfony's authentication (possibly together with the new Authenticator-based Security) without invoking Doctrine anywhere?
I know that I must implement a UserLoaderInterface, but the docs use Doctrine so heavily that I cannot figure out how to do it without.
The post I just mentioned is asking something similar, but it still uses Symfony 2 and is thus too outdated.
I have a database that has the necessary User table with the usual columns (ID, eMail, Password, Name, etc.).
To the point:
How can I use Symfony's authentication (possibly together with the new Authenticator-based Security) without Doctrine?
To configurate that is on the the official website and also on this tutorial in SymfonyCast, but basically you can authenticate the user as you want:
See the next example:
Create a file on src\App\Security folder if your configuration is using the default config and create the class TokenAuthenticator, now see the below code, in this case check the class App\Service\ExternalAuthenticator, who will be in charge to get the information from other service or api and the return.
<?php
namespace App\Security;
use App\Example\Student;
use App\Service\ExternalAuthenticator;
use App\DTO\INFORMATIONFROMOTHERSERVICE;
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\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Security;
final class TokenAuthenticator extends AbstractGuardAuthenticator
{
/** #var Security */
private $security;
/** #var ExternalAuthenticator */
private $externalAuthenticator;
/** #var UrlGeneratorInterface */
private $urlGenerator;
public function __construct(
Security $security,
ExternalAuthenticator $externalAuthenticator
) {
$this->security = $security;
$this->externalAuthenticator = $externalAuthenticator;
}
/**
* {#inheritDoc}
*/
public function supports(Request $request)
{
//on this example, this guard must be using if on the request contains the word token
$response = false;
$apiKey = $request->query->get('token');
if (!is_null($apiKey)) {
$response = true;
}
return $response;
}
/**
* {#inheritDoc}
*/
public function getCredentials(Request $request)
{
$apiKey = $request->query->get('token');
// Validate with anything you want, other service or api
/** #var INFORMATIONFROMOTHERSERVICE**/
$dtoToken = $this->externalAuthenticator->validateToken($apiKey, $simulator);
return $dtoToken;
}
/**
* #param INFORMATIONFROMOTHERSERVICE $credentials
* #param UserProviderInterface $userProvider
* #return INFORMATIONFROMOTHERSERVICE |UserInterface|null
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider;
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new RedirectResponse($this->urlGenerator->generate('home_incorrect'));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
return new RedirectResponse($request->getPathInfo());
}
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse($this->urlGenerator->generate('home_incorrect'));
}
public function supportsRememberMe()
{
// todo
}
}
Now the external service must return App\DTO\INFORMATIONFROMOTHERSERVICE class, but this class must implement the UserInterface, now with this in mind. We need to configurate what guard must be in charge of what routes, see the next example:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
//You can use a
custom_provider:
id : App\DTO\INFORMATIONFROMOTHERSERVICE
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
survey:
anonymous: true
pattern: ^/(custom_path)/
// The
provider: custom_provider
guard:
// You can use as many authenticator that you want, but in the node entrypoint, you must choose who must be the default if only is one you could remove the entrypoint node, similar as the main firewall
authenticators:
- App\Security\TokenAuthenticator
- App\Security\OtherAuthenticator
entry_point: App\Security\OtherAuthenticator
main:
anonymous: true
lazy: true
provider: app_user_provider
logout:
path: app_logout
guard:
authenticators:
- App\Security\AppAuthenticator
Also see the next documentation, that will guide you to create the class App\DTO\INFORMATIONFROMOTHERSERVICE.
I hope this answer, help you

Why credentials are null when erase_credentials is false?

Symfony 5.3
security.yaml
security:
...
erase_credentials: false
LoginListener.php
<?php
namespace App\EventListener;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class LoginListener
{
private $passwordHasherFactory;
private $em;
public function __construct(PasswordHasherFactoryInterface $passwordHasherFactory, EntityManagerInterface $em)
{
$this->passwordHasherFactory = $passwordHasherFactory;
$this->em = $em;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
$token = $event->getAuthenticationToken();
// Migrate the user to the new hashing algorithm if is using the legacy one
if ($user->hasLegacyPassword()) {
// Credentials can be retrieved thanks to the false value of
// the erase_credentials parameter in security.yml
$plainPassword = $token->getCredentials();
file_put_contents('darius.txt', 'test'.$plainPassword, FILE_APPEND); // why null?
}
$token->eraseCredentials();
}
}
https://symfony.com/doc/current/reference/configuration/security.html#erase-credentials
If true, the eraseCredentials() method of the user object is called
after authentication.
So probably if false it should not erase? Why it is erasing?
Password is received because login works. I just dissapears at some point.
Update
Question is why credentials are null before calling
$token->eraseCredentials();
Managed to install xdebug and found that when creating token the credentials are not set:
JsonLoginAuthenticator:
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
}
UsernamePasswordToken:
public function __construct($user, $credentials, string $firewallName, array $roles = [])
{
parent::__construct($roles);
if ('' === $firewallName) {
throw new \InvalidArgumentException('$firewallName must not be empty.');
}
$this->setUser($user);
$this->credentials = $credentials;
$this->firewallName = $firewallName;
parent::setAuthenticated(\count($roles) > 0);
}
So I guess that is the problem - under security - firewall there is such setup:
main:
lazy: true
provider: app_user_provider
logout:
path: logout
target: after_logout
json_login:
check_path: /login
entry_point: App\Security\AuthenticationEntryPoint
So probably I have answered my question why there is no credentials. I am just missing now how do create new password hash for password on login, but that is probably for different question.

symfony3 guard login form doesn't authenticate [duplicate]

This question already has an answer here:
Symfony & Guard: "The security token was removed due to an AccountStatusException"
(1 answer)
Closed 5 years ago.
I try to make a form login authentication with guard (symfony 3.2) but it doesn't work.
The authentication is working, but when I'm redirected to the home page (accueil), I'm redirected to the login page without anthentication.
If I put in the controler of my home page
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
I can see my user, the role but he is not authenticated.
DashboardController.php on line 23:
PostAuthenticationGuardToken {#133 ▼
-providerKey: "main"
-user: User {#457 ▶}
-roles: array:1 [▼
0 => Role {#120 ▼
-role: "ROLE_SUPERADMIN"
}
]
-authenticated: false
-attributes: []
}
What I've missed ?
Security.ym
security:
encoders:
EntBundle\Entity\User\User:
algorithm: bcrypt
providers:
database:
entity:
class: EntBundle:User\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
logout: ~
guard:
authenticators:
- ent.login_authenticator
TestAuthenticator.php
namespace EntBundle\Security;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class TestAuthenticator extends AbstractGuardAuthenticator
{
private $em;
private $router;
public function __construct(EntityManager $em, RouterInterface $router)
{
$this->em = $em;
$this->router = $router;
}
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
return $this->em->getRepository('EntBundle:User\User')->findOneBy(['username' => $username]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// this is just for test
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('accueil');
return new RedirectResponse($url);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function supportsRememberMe()
{
return false;
}
}
DashboardController.php
namespace EntBundle\Controller\Dashboard;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DashboardController extends Controller
{
/**
* #Route("/accueil", name="accueil")
*/
public function indexAction()
{
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
return $this->render('EntBundle:dashboard:dashboard_structure.html.twig');
}
/**
* #Route("/login", name="login")
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('EntBundle::login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
/**
* #Route("/logout", name="logout")
*/
public function logoutAction()
{
}
}
EDIT:
Thanks leo_ap for your help but the problem doesnt come from there.
The config session is like this :
session:
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
and if I check in the save path folder I have session file created but not authenticated.
_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:29:"http://localhost:8000/accueil";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1488245179;s:1:"c";i:1488244922;s:1:"l";s:1:"0";}
If I try the normal login_form with security.yml it's working fine...
I've try with handler_id and save_path at null with no success.
EDIT2:
I've found why I'm always redirected to the login page, because I'm logged out!
[2017-02-28 09:16:34] security.INFO: The security token was removed due to an AccountStatusException. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException(code: 0): at /home/philippe/Documents/symfony/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"}
and in GuardAuthenticationProvider.php (86)
The listener *only* passes PreAuthenticationGuardToken instances.
This means that an authenticated token (e.g.PostAuthenticationGuardToken)
is being passed here, which happens if that token becomes "not authenticated" (e.g. happens if the user changes between requests).
In this case, the user should be logged out, so we will return an AnonymousToken to accomplish that.
But Why ???
May be your Session that isn't persisting the token. Check your Session configuration, inside: config.yml. in the framework option, there is session. See how the handler_id and save_path are configured. It may be that your php instalation is unable to handle the sessions on the configured path. Try to put null to handler_id and save_path to force php use its own build in configurations to handle sessions.
config.yml file:
framework:
{ .. Other configurations ..}
session:
handler_id: null
save_path: null
{ .. More configurations ..}

Symfony2 + functional test + authentication

I want to prepare simple function test in PHPUnit for my symfony 2 project: authenticate user and display some page without redirecting to login form.
I use LDAP user provider (IMAG\LdapBundle\Provider\LdapUserProvider).
My firewalls configuration in security.yml:
firewalls:
secured_area:
pattern: ^/
provider: ldap
imag_ldap:
login_path: /login
logout:
path: /logout
target: /
In my test controller I have following methods:
protected function createClientWithAuthentication($firewallName, array $options = array(), array $server = array())
{
$client = $this->createClient($options, $server);
$client->getCookieJar()->set(new \Symfony\Component\BrowserKit\Cookie(session_name(), true));
$user = $this->getCurrentUser($client);
$token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles());
$client->getContainer()->get('session')->set('_security_' . $firewallName, serialize($token));
return $client;
}
protected function getCurrentUser($client) {
$userProvider = $client->getContainer()->get('imag_ldap.security.user.provider');
$user = $userProvider->loadUserByUsername('my.login');
return $user;
}
Then I have simple test:
public function testProfile()
{
$c = $this->createClientWithAuthentication('secured_area');
$c->request('GET', '/profile');
$this->assertEquals($c->getResponse()->getStatusCode(), 200);
}
Test fails:
Failed asserting that 200 matches expected 302.
I digged it and I found out that I receive complete user from user provider (all fields are properly filled). But response is redirecting to localhost/login page.
Please tell me what else I can test... where is the place where decision is made if login page should (or not) be displayed?
I have WebTestCase base class that creates a authorized client. You can adapt it to your purpose ...
class BaseWebTestCase extends WebTestCase{
protected $client;
protected function setUp(){
$this->client=$this->createAuthorizedClient();
}
/**
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected function createAuthorizedClient()
{
$client = static::createClient();
$container = $client->getContainer();
/** #var Person $user */
$user=$container->get("netnotes.cust.person")->getRepository()->getSuperAdmin();
$session = $container->get('session');
/** #var $userManager \FOS\UserBundle\Doctrine\UserManager */
$userManager = $container->get('fos_user.user_manager');
/** #var $loginManager \FOS\UserBundle\Security\LoginManager */
$loginManager = $container->get('fos_user.security.login_manager');
$firewallName = $container->getParameter('fos_user.firewall_name');
$user = $userManager->findUserBy(array('username' => $user->getUsername()));
$loginManager->loginUser($firewallName, $user);
// save the login token into the session and put it in a cookie
$container->get('session')->set('_security_' . $firewallName,
serialize($container->get('security.context')->getToken()));
$container->get('session')->save();
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
return $client;
}
}

Redirecting to default_target_path in custom LoginSuccessHandler?

I have a custom success login handler for custom role-based redirect after successful login.
When on of these special roles in $specialRoles array is found, a new RedirectResponse is returned:
/** #DI\Service("handler.login_sucess_handler") */
class LoginSuccessHandler implements uthenticationSuccessHandlerInterface
{
private $router;
private $securityContext;
/**
* #DI\InjectParams({
* "securityContext"= #DI\Inject("security.context"),
* "router" = #DI\Inject("router") })
*/
public function __construct(SecurityContext $securityContext,
I18nRouter $router)
{
$this->securityContext = $securityContext;
$this->router = $router;
}
public function onAuthenticationSuccess(Request $request,
TokenInterface $token)
{
$specialRoles = array('ROLE_1', 'ROLE_2', 'ROLE_3');
foreach($specialRoles as $specialRole)
{
if($this->securityContext->isGranted($specialRole))
{
$redirectUrl = $this->router->generate('manage_index');
return new RedirectResponse($redirectUrl);
}
}
}
}
On the other hand, i need to specify what happens if user has no special roles. I'd like to make it more flexible, i.e: not hardcoding the default /app/dashboard route. That is, read the default_target_path (if any) from security.yml file:
form_login:
login_path: /app/login
check_path: /app/login_check
default_target_path: /app/dashboard
success_handler: handler.login_sucess_handler
Is there any way for doing this?
Of course leaving the code as is, when an user hasn't any special role, an exception is thrown:
Catchable Fatal Error: Argument 1 passed to
Symfony\Component\HttpKernel\Event\GetResponseEvent::setResponse()
must be an instance of Symfony\Component\HttpFoundation\Response, null
given.
This is how I did it in one of my projects:
if ($targetPath = $request->getSession()->get('_security.target_path')) {
return new RedirectResponse($targetPath);
}
Why don't you simply add return statement with needed route after foreach loop?
public function onAuthenticationSuccess(
Request $request,
TokenInterface $token
)
{
$specialRoles = array('ROLE_1', 'ROLE_2', 'ROLE_3');
foreach($specialRoles as $specialRole) {
if ($this->securityContext->isGranted($specialRole)) {
$redirectUrl = $this->router->generate('manage_index');
return new RedirectResponse($redirectUrl);
}
}
$redirectUrl = $this->router->generate('your_custom_route');
return new RedirectResponse($redirectUrl);
}
Don't forget to define your custom route in routing, of course.
EDIT:
You could also set _referer input field in your login form and submit it with your form, and then get it in action by $request->get('_referer');
Another way is to try to get referer from headers: $request->headers->get('Referer');
And, finally, you could get default target path, which is defined in security.yml: $request->getSession()->get('_security.target_path');
Did you solve the redirect problem? These solutions were not working for me at all, but this one:
return new RedirectResponse($request->headers->get('Referer'));

Resources