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
Related
My remember function for my symfony application seems to be not working appropriately. I've followed the resource provided by Symfony itself here.
Anyway, here is part of my security.yaml file:
security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
form_login:
login_path: /p/login
entry_point: form_login
# login_throttling:
# limiter: app.custom.limiter
lazy: true
provider: app_user_provider
# https://symfony.com/doc/current/security/impersonating_user.html
switch_user: true
custom_authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: logout
# where to redirect after logout
# target: app_any_route
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
token_provider:
doctrine: true
My LoginFormAuthenticator.php file:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\RateLimiter\RateLimiterFactory;
class LoginFormAuthenticator extends AbstractAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordHasherInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request): ?bool
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function authenticate(Request $request): Passport
{
$email = $request->request->get('email');
$password = $request->request->get('password');
$csrfToken = $request->request->get('_csrf_token');
return new Passport(
new UserBadge($email),
new PasswordCredentials($password),
[
new CsrfTokenBadge('authenticate', $csrfToken),
new RememberMeBadge()
],
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Invalid credentials.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('feed', ['page_num' => 1]));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->urlGenerator->generate('login'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
The database schema that was created by using the doctrine as the token provider:
The user gets logged in but then logs out automatically after an hour (I believe) which has gotten annoying. Even though 1 week has been specified in the security.yaml file.
UPDATE
After reading threads via github (here), it seems this approach isn't viable. What alternative should we use to allow our users to stay logged in within the specified duration that we note in our security.yaml file?
Consider that if we use cookies or regular sessions for user log ins, every user will be logged out every time we deploy because of the cache:clear function.
Goto user entity and find getUserIdentyfier() function. Make sure this returns email.
Property: email<
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.
I want to implement the following authentication scenario in symfony 5:
User sends a login form with username and password, authentication is processed against an LDAP server
if authentication against the LDAP server is successful :
if there is an instance of my App\Entity\User that as the same username as the ldap matching entry, refresh some of its attributes from the ldap server and return this entity
if there is no instance create a new instance of my App\Entity\User and return it
I have implemented a guard authenticator which authenticates well against the LDAP server but it's returning me an instance of Symfony\Component\Ldap\Security\LdapUser and I don't know how to use this object to make relation with others entities!
For instance, let's say I have a Car entity with an owner property that must be a reference to an user.
How can I manage that ?
Here is the code of my security.yaml file:
security:
encoders:
App\Entity\User:
algorithm: auto
# 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
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: "%env(LDAP_BASE_DN)%"
search_dn: "%env(LDAP_SEARCH_DN)%"
search_password: "%env(LDAP_SEARCH_PASSWORD)%"
default_roles: ROLE_USER
uid_key: uid
extra_fields: ['mail']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: my_ldap
guard:
authenticators:
- App\Security\LdapFormAuthenticator
I finally found a good working solution.
The missing piece was a custom user provider.
This user provider has the responsibility to authenticate user against ldap and to return the matching App\Entity\User entity. This is done in getUserEntityCheckedFromLdap method of LdapUserProvider class.
If there is no instance of App\Entity\User saved in the database, the custom user provider will instantiate one and persist it. This is the first user connection use case.
Full code is available in this public github repository.
You will find below, the detailed steps I follow to make the ldap connection work.
So, let's declare the custom user provider in security.yaml.
security.yaml:
providers:
ldap_user_provider:
id: App\Security\LdapUserProvider
Now, configure it as a service, to pass some ldap usefull string arguments in services.yaml.
Note since we are going to autowire the Symfony\Component\Ldap\Ldap service, let's add this service configuration too:
services.yaml:
#see https://symfony.com/doc/current/security/ldap.html
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: ldap
port: 389
# encryption: tls
options:
protocol_version: 3
referrals: false
App\Security\LdapUserProvider:
arguments:
$ldapBaseDn: '%env(LDAP_BASE_DN)%'
$ldapSearchDn: '%env(LDAP_SEARCH_DN)%'
$ldapSearchPassword: '%env(LDAP_SEARCH_PASSWORD)%'
$ldapSearchDnString: '%env(LDAP_SEARCH_DN_STRING)%'
Note the arguments of the App\Security\LdapUserProvider come from env vars.
.env:
LDAP_URL=ldap://ldap:389
LDAP_BASE_DN=dc=mycorp,dc=com
LDAP_SEARCH_DN=cn=admin,dc=mycorp,dc=com
LDAP_SEARCH_PASSWORD=s3cr3tpassw0rd
LDAP_SEARCH_DN_STRING='uid=%s,ou=People,dc=mycorp,dc=com'
Implement the custom user provider :
App\Security\LdapUserProvider:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class LdapUserProvider implements UserProviderInterface
{
/**
* #var Ldap
*/
private $ldap;
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var string
*/
private $ldapSearchDn;
/**
* #var string
*/
private $ldapSearchPassword;
/**
* #var string
*/
private $ldapBaseDn;
/**
* #var string
*/
private $ldapSearchDnString;
public function __construct(EntityManagerInterface $entityManager, Ldap $ldap, string $ldapSearchDn, string $ldapSearchPassword, string $ldapBaseDn, string $ldapSearchDnString)
{
$this->ldap = $ldap;
$this->entityManager = $entityManager;
$this->ldapSearchDn = $ldapSearchDn;
$this->ldapSearchPassword = $ldapSearchPassword;
$this->ldapBaseDn = $ldapBaseDn;
$this->ldapSearchDnString = $ldapSearchDnString;
}
/**
* #param string $username
* #return UserInterface|void
* #see getUserEntityCheckedFromLdap(string $username, string $password)
*/
public function loadUserByUsername($username)
{
// must be present because UserProviders must implement UserProviderInterface
}
/**
* search user against ldap and returns the matching App\Entity\User. The $user entity will be created if not exists.
* #param string $username
* #param string $password
* #return User|object|null
*/
public function getUserEntityCheckedFromLdap(string $username, string $password)
{
$this->ldap->bind(sprintf($this->ldapSearchDnString, $username), $password);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$search = $this->ldap->query($this->ldapBaseDn, 'uid=' . $username);
$entries = $search->execute();
$count = count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$ldapEntry = $entries[0];
$userRepository = $this->entityManager->getRepository('App\Entity\User');
if (!$user = $userRepository->findOneBy(['userName' => $username])) {
$user = new User();
$user->setUserName($username);
$user->setEmail($ldapEntry->getAttribute('mail')[0]);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
return $user;
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside ' . __FILE__);
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass($class)
{
return User::class === $class || is_subclass_of($class, User::class);
}
}
Configure the firewall to use our custom user provider:
security.yaml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: ldap_user_provider
logout:
path: app_logout
guard:
authenticators:
- App\Security\LdapFormAuthenticator
Write an authentication guard:
App\SecurityLdapFormAuthenticator:
<?php
namespace App\Security;
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\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $urlGenerator;
private $csrfTokenManager;
public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
{
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $userProvider->getUserEntityCheckedFromLdap($credentials['username'], $credentials['password']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
//in this scenario, this method is by-passed since user authentication need to be managed before in getUser method.
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$request->getSession()->getFlashBag()->add('info', 'connected!');
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
My user entity looks like this:
`App\Entity\User`:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password = 'password is not managed in entity but in ldap';
/**
* #ORM\Column(type="string", length=255)
*/
private $userName;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function setUserName(string $userName): self
{
$this->userName = $userName;
return $this;
}
}
For Symfony 6 I do like this.
No extra implementation
security:
role_hierarchy:
ROLE_USER: ROLE_USER
ROLE_ADMIN: [ROLE_USER, ROLE_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
pmh_db:
entity:
class: App\Entity\User
property: username
pmh_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%base_dn%'
search_dn: '%search_dn%'
search_password: '%search_password%'
default_roles: 'ROLE_USER'
uid_key: '%uid_key%'
extra_fields: ['email']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
pattern: ^/
provider: pmh_db
switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
login_throttling:
max_attempts: 5
form_login_ldap:
login_path: app_login
check_path: app_login
service: Symfony\Component\Ldap\Ldap
dn_string: 'DOMAIN\{username}'
query_string: null
default_target_path: /
logout:
path: /logout
target: /
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
# by default, the feature is enabled by checking a
# checkbox in the login form (see below), uncomment the
# following line to always enable it.
always_remember_me: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: '^/login', roles: PUBLIC_ACCESS }
- { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
- { path: '^/', roles: ROLE_USER }
I am writing regarding the Symfony authentication problem, which occurred last month and I still cannot find a solution, so I am dependent on you :D
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_users")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
//id,username,password
public function getSalt()
{
return null;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt,
));
}
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized);
}
}
This is my User entity and now below you can see my security.yaml which I think I configured right:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|img|js)/
security: false
main:
anonymous: true
http_basic: ~
provider: db_provider
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
Whenever I am trying to access /admin route it shows me http-basic login but whenever I input "admin, admin" nothing happens. IN my database I have one user with username:admin and password admin which is hashed by bcrypt.
Not using authentication then everything works as it should, I get all data from the database as it should be after authentication.
Thanks for your help guys!
Your problem
As Med already pointed out, your User entity has the ROLE_USER role as default:
/* App/Entity/User.php */
public function getRoles()
{
return array('ROLE_USER');
}
Your access_control configuration on the other hand states that the route /admin can only be accessed with a user that has the ROLE_ADMIN role:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
That means, your user "admin" lacks the sufficient role to access /admin.
Solution
You need to be able to assign multiple roles to the user. One possible way is saving the roles as a concatenated string and returning it as an array:
/* App/Entity/User.php */
/**
* #ORM\Column(name="roles", type="string")
* #var string
*/
private $roles;
/**
* Get the user roles as an array of strings
* #return array
*/
public function getRoles()
{
return explode($roles, ',');
}
You can even add some methods to manage your roles via the entity class:
/* App/Entity/User.php */
/**
* Add a new role
* #param string $role name of the role
* #return this
*/
public function addRole($role)
{
$roles = $this->getRoles();
if (array_search($role, $roles) === false) {
$roles[] = $role;
$this->roles = implode(',', $roles);
}
return $this;
}
/**
* Remove a role
* #param string $role name of the role
* #return this
*/
public function removeRole($role)
{
$roles = $this->getRoles();
$searchResult = array_search($role, $roles);
if ($searchResult !== false) {
unset($roles[$searchResult]);
$this->roles = implode(',', $roles);
}
return $this;
}
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 ..}