I have a Symfony 4.4 project. Login, authentication, and authorization work fine on web.
To simulate authentication in unit tests, I'm using Symfony's example.
...but that's not working. The user is not authenticated in the unit test. I get an Unauthorized with error message "Full authentication is required to access this resource."
My test:
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
class DefaultControllerTest extends WebTestCase
{
private $client = null;
public function setUp()
{
$this->client = static::createClient();
}
public function testJsonUserEndpoint()
{
$this->logIn();
$crawler = $this->client->request('GET', '/json/user');
$this->assertNotContains('invalid_credentials', $this->client->getResponse()->getContent());
}
private function logIn()
{
$user = self::$container->get(EntityManagerInterface::class)->getRepository(User::class)->findOneByMail('test106#test.com');
$session = $this->client->getContainer()->get('session');
$firewallName = 'main';
$firewallContext = 'main';
$token = new PostAuthenticationGuardToken($user, $firewallName, ['ROLE_USER']);
$session->set('_security_'.$firewallContext, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
}
My firewall:
main:
anonymous:
secret: '%env(APP_SECRET)%'
remember_me:
always_remember_me: true
secret: '%env(APP_SECRET)%'
provider: session_user_provider
guard:
authenticators:
- App\Security\Authenticator\LoginAuthenticator
- App\Security\Authenticator\MobileBridgeAuthenticator
- App\Security\Authenticator\BearerTokenAuthenticator
- App\Security\Authenticator\HashAuthenticator
- App\Security\Authenticator\ClientAuthenticator
entry_point: App\Security\Authenticator\LoginAuthenticator
logout:
path: execute_logout
target: render_login
Unit test failure:
1) App\Tests\Controller\DefaultControllerTest::testJsonUserEndpoint
Failed asserting that '{"status":"invalid_credentials","errors":["Invalid Credentials"],"debug":"Full authentication is required to access this resource.","error":"Unauthorized"}' does not contain "invalid_credentials".
How can I authenticate a user in my unit tests without using the login form?
The problem was that I don't store roles in the database with the user -- they're added in my authenticators -- and passing the roles to PostAuthenticationGuardToken wasn't enough. I also had to add them to the user in the token:
$user->setRoles(['ROLE_USER']);
$token = new PostAuthenticationGuardToken($session_user, $firewallName, ['ROLE_USER']);
Actually, it didn't even matter what's in the Token's roles as long as it's an array:
$user->setRoles(['ROLE_USER']);
$token = new PostAuthenticationGuardToken($session_user, $firewallName, []);
Related
Is ist possible to configure user authentification for Symfony 5.4 using either User/Password stored in the User entity oder LDAP depending on a boolean field or the password being null in the User entity?
I need to create some users that have to log on but are not contained in the customers LDAP structure. LDAP is more a comfort thing (single credentials for all apps) here than a security one (no one may logon if not defined in LDAP).
Perhaps I can get around programming the security things from the scatch and just combine two different providers.
Meanwhile I solved it and it was quite easy by using the "normal" password authenticator and modifying a bit of code. The strategy is:
Check if its an LDAP user. If not, use password authentication
Search the user in the LDAP directory
Bail out if not found
Bail out if not unique
Check credentials
The steps I took:
I added a boolean field to the entity USER called ldap_flag
I added variables to .env to specify the LDAP parameters
I modified Security/LoginFormAuthenticator:checkCredentials like this:
if ($user->getLDAPFlag()) {
if ($conn = ldap_connect($_ENV['LDAP_HOST'])) {
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, $_ENV['LDAP_PROTOCOL_VERSION']);
ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
if ($_ENV['LDAP_CERT_CHECK'] == 0)
ldap_set_option($conn, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
$dn = $_ENV['LDAP_BIND_DN'];
$pw = $_ENV['LDAP_BIND_PW'];
if (ldap_bind($conn, $dn, $pw)) {
// Search user
$res = ldap_search($conn, $_ENV['LDAP_SEARCH_DN'], "(&(uid=" . $user->getUserName() . ")(objectClass=inetOrgPerson))", array('dn'));
$entries = ldap_get_entries($conn, $res);
if ($entries["count"] == 1)
return ldap_bind($conn, $entries[0]['dn'], $credentials['password']);
else if ($entries["count"] > 0)
throw new CustomUserMessageAuthenticationException('Benutzer im LDAP nicht eindeutig!');
else
throw new CustomUserMessageAuthenticationException('Benutzer auf dem LDAP Server nicht gefunden!');
} else
// cannot bind
throw new CustomUserMessageAuthenticationException('Kann nicht an LDAP-Server binden!');
ldap_unind($conn);
} else {
// no LDAP Connection
throw new CustomUserMessageAuthenticationException('Keine Verbindung zum LDAP-Server');
}
} else
// internal password-check
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
The error messages are in German but it should be easy to adapt them to an other language as they explain within their context.
I have found another solution, which makes use of Symfony's services. But it is not a one liner. One has to define several configurations, override some services and create two custom classes.
But this advices should be relatively complete.
# config/packages/security.yaml
security:
enable_authenticator_manager: true
providers:
all_users:
chain:
providers: [ldap_users, local_users]
local_users:
entity:
class: App\Entity\User
property: username
ldap_users:
# in services.yml Symfony's provider is overwritten with
# App\Security\LdapUserProvider
ldap:
service: Symfony\Component\Ldap\Ldap # see services.yml
base_dn: '%env(LDAP_BASE_DN)%'
search_dn: '%env(LDAP_SEARCH_DN)%'
search_password: '%env(LDAP_SEARCH_PASSWORD)%'
default_roles: ROLE_USER
uid_key: '%env(LDAP_UID_KEY)%'
firewalls:
main:
pattern: ^/
lazy: true
provider: all_users
form_login_ldap:
check_path: app_login
login_path: app_login
service: Symfony\Component\Ldap\Ldap # see services.yml
dn_string: '%env(LDAP_BASE_DN)%'
search_dn: '%env(LDAP_SEARCH_DN)%'
search_password: '%env(LDAP_SEARCH_PASSWORD)%'
query_string: 'sAMAccountName={username}'
# config/services.yaml
services:
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
tags:
- ldap
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: '%env(LDAP_HOST)%'
port: 389
encryption: none
options: { protocol_version: 3, referrals: false, network_timeout: 5 }
# overwrite symfony's LdapUserProvider so that a User entity is used
# instead of the default user class of Symfony.
security.user.provider.ldap:
class: App\Security\LdapUserProvider
arguments: [~, ~, ~, ~, ~, ~, ~, ~, ~]
App\Security\AppCredentialsCheckListener:
decorates: 'security.listener.form_login_ldap.main'
arguments:
$checkLdapCredentialsListener: '#.inner'
$checkCredentialsListener: '#security.listener.check_authenticator_credentials'
// src/Security/LdapUserProvider.php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Ldap\Security\LdapUserProvider as BaseLdapUserProvider;
/**
* This service is responsible for adding a user entity to the local database.
*/
class LdapUserProvider extends BaseLdapUserProvider
{
private EntityManagerInterface $entityManager;
private UserRepository $userRepo;
public function __construct(
LdapInterface $ldap,
string $baseDn,
string $searchDn,
string $searchPassword,
array $defaultRoles,
string $uidKey,
string $filter,
?string $passwordAttribute,
?array $extraFields,
EntityManagerInterface $entityManager,
UserRepository $userRepo
) {
parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
$this->entityManager = $entityManager;
$this->userRepo = $userRepo;
}
protected function loadUser(string $username, Entry $entry)
{
$ldapUser = parent::loadUser($username, $entry);
$user = $this->userRepo->findOneBy(['username' => $ldapUser->getUsername()]);
$flush = false;
if (!$user) {
$user = new User();
$user->setUsername($ldapUser->getUsername());
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
}
// src/Security/AppCredentialsCheckListener.php
namespace App\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener;
use Symfony\Component\Ldap\Security\LdapBadge;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener;
/**
* This event listener is responsible for checking the password.
* First the LDAP password is checked and as a fallback the local
* password is checked
*/
class AppCredentialsCheckListener implements EventSubscriberInterface
{
private CheckLdapCredentialsListener $checkLdapCredentialsListener;
private CheckCredentialsListener $checkCredentialsListener;
public function __construct(
CheckLdapCredentialsListener $checkLdapCredentialsListener,
CheckCredentialsListener $checkCredentialsListener
) {
$this->checkLdapCredentialsListener = $checkLdapCredentialsListener;
$this->checkCredentialsListener = $checkCredentialsListener;
}
public static function getSubscribedEvents(): array
{
// priority must be higher than the priority of the Symfony listeners
return [CheckPassportEvent::class => ['onCheckPassport', 999]];
}
public function onCheckPassport(CheckPassportEvent $event)
{
try {
// Check ldap password
$this->checkLdapCredentialsListener->onCheckPassport($event);
} catch (BadCredentialsException $e) {
// Fallback to local entity password
$this->checkCredentialsListener->checkPassport($event);
// We have to mark the ldap badge as resolved. Otherwise an exception will be thrown.
/** #var LdapBadge $ldapBadge */
$ldapBadge = $event->getPassport()->getBadge(LdapBadge::class);
$ldapBadge->markResolved();
}
}
}
I have added some comments to the config and the code, which should make clear how it is achieved. I hope it helps anyone.
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.
I am having trouble finding any help, examples or tutorials that layout how to use CAS for Authorization but load the roles from a local database in Symfony 4.
I managed to authenticate via CAS using the bundle: prayno. I manage to succesfully get roles from a database via an entity and repository. But the fact is that I did not manage to make the link between the twos.
Here is my security.yaml:
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from- user-providers
providers:
cas:
id: prayno.cas_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout: ~
guard:
authenticators:
- prayno.cas_authenticator
The function under the prayno bundle to get authentification and the roles:
public function loadUserByUsername($username)
{
if ($username) {
$password = '...';
$salt = "";
$roles = ['ROLE_USER'];
return new CasUser($username, $password, $salt, $roles);
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
From there, I tried to get my roles from the database using the controller I created for this occasion:
<?php
namespace App\Controller;
use App\Entity\FscDroitsBundle;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class RightsController extends AbstractController
{
/**
* #Route("/rights", name="rights")
*/
public function attribute_rights($netid)
{
$entities = $this->em->getDoctrine()->getRepository(FscDroitsBundle::class)->findRights($netid);
$rights_array = array('ROLE_USER');
foreach ($entities as $data) {
array_push($rights_array, $data->getBundle());
}
return $rights_array;
}
But, until now, I did not find any way to use this controller in the bundle, something like:
$rights = new RightsController();
$roles = $rights->attribute_rights($username);
instead of:
$roles = ['ROLE_USER'];
in my loadbyUsername function.
The problem seems to come from this line:
$entities = $this->getDoctrine()->getRepository(FscDroitsBundle::class)->findRights($netid);
The error is: Call to a member function has() on null
How is it that the controller can not access the repository in the bundle meanwhile it access it without any problem on a view ?
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);
I'm embedding the FosUserBundle login inside my home page, i have overriddent the security controller of fos and changed the renderLogin() action, i had to put an if condition to redirect to last accessed page using referers , that was all well and good, but i realize now that The HTTP Referer header is not required by the HTTP Protocol and it can be compleatly skipped or even spoofed by browser setting etc. its unreliable!
but if symfony framework can guarantee $request->server->get('HTTP_REFERER') or $request->headers->get('referer') will be set. i can use these without hassle
my question to SO
is the referer from symfony request object 100% reliable?
what is the difference between $request->server->get('HTTP_REFERER') and $request->headers->get('referer') ?
what could be alternatives if they are not reliable?
(P.S)
in symfony docs
if the user requested http://www.example.com/admin/post/18/edit, then after they successfully log in, they will eventually be sent back to http://www.example.com/admin/post/18/edit. This is done by storing the requested URL in the session.
but they have't explained the inner working of it. if referers are finally proved to be unreliable then my alternatives are as below, any suggestion are welcomed
1). registering a listner and adding an attribute last_path
2). storing a session variable last_path
in security.yml
main:
pattern: ^/
logout: true
form_login:
provider: fos_userbundle
login_path: /login
success_handler: authentication_handler
failure_handler: authentication_handler
remember_me:
key: secret
lifetime: 604800
path: /
domain: yourdomain.com
anonymous: true
logout:
path: /logout
target: /
handler: authentication_handler
in the config.yml
services:
authentication_handler:
class: YourBundle\UserBundle\Security\AuthenticationHandler
the AuthentificationHandler Class
<?PHP
Namespace YourBundle\UserBundle\Security;
class AuthenticationHandler implements
AuthenticationFailureHandlerInterface, AuthenticationSuccessHandlerInterface, LogoutSuccessHandlerInterface {
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) {
$referer = $request->headers->get('referer');
$request->getSession()->setFlash('LoginError', $exception->getMessage());
return new RedirectResponse($referer);
}
public function onAuthenticationSuccess(Request $request, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token) {
$referer = $request->headers->get('referer');
$request->getSession()->setFlash('LoginError', "success");
return new RedirectResponse($referer);
}
public function onLogoutSuccess(Request $request) {
$referer = $request->headers->get('referer');
return new RedirectResponse($referer);
}
}
?>
You can save the URL requested before you log in session, and then if successful redirect the user to the desired URL.
The URL is captured and saved in the session start method and then recovered in onAuthenticationSuccess method.
obs .: It will be shown below only the portion of code required for understanding the solution, the code in its entirety can be accessed at: http://symfony.com/doc/current/security/guard_authentication.html
<?php
namespace VER\VerCoreBundle\Security;
class FormAuthenticator extends AbstractGuardAuthenticator
{
/**
* {#inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('_welcome');
$previous = $request->getSession()->get('previous');
if ($previous) {
$url = $previous;
}
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$request->getSession()->set('previous', $request->getUri());
$url = $this->router->generate('ver_core_login');
return new RedirectResponse($url);
}
}
?>