I want to use ConstraintValidator in my custom login authentication (using SimpleForm) to validate google Recaptcha of this bundle EWZRecaptchaBundle I have not an idea
security.yaml main firewall section:
providers:
default:
entity:
class: App:User
property: phone
main:
pattern: ^/
anonymous: ~
provider: default
simple_form:
authenticator: App\Security\Authenticator\UserAuthenticator
check_path: login
login_path: login
username_parameter: phone
password_parameter: password
use_referer: true
logout:
path: logout
I need to use Validaitor in App\Security\Authenticator\UserAuthenticator
This is my Custom Authenticator (App\Security\Authenticator\UserAuthenticator):
//...
class UserAuthenticator implements SimpleFormAuthenticatorInterface
{
private $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
}
catch (UsernameNotFoundException $exception) {
throw new CustomUserMessageAuthenticationException("invalid");
}
$isPasswordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());
if ($isPasswordValid) {
return new UsernamePasswordToken($user, $user->getPassword(), $providerKey, $user->getRoles());
}
throw new CustomUserMessageAuthenticationException("invalid");
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken && $token->getProviderKey() === $providerKey;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UsernamePasswordToken($username, $password, $providerKey);
}
}
Check out How to Create a Custom Authentication System with Guard for a simpler and more flexible way to accomplish custom authentication tasks like this.
Especially the getCredentials method of the GuardAuthenticator class that you will create.
getCredentials(Request $request)
This will be called on every request and your job is to read the token (or whatever your "authentication" information is) from the request and return it. These credentials are later passed as the first argument of getUser().
or whatever your "authentication" information is so you will be able to handle the value passed within the recaptcha.
<?php
namespace App\Security\Authenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
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\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Validator\Validator\Validator\ValidatorInterface;
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
/**
* #var UserPasswordEncoderInterface
*/
private $encoder;
/**
* #var ValidatorInterface
*/
private $validator;
/**
* FormLoginAuthenticator constructor.
* #param UserPasswordEncoderInterface $encoder
* #param IsTrueValidator $isTrueValidator
*/
public function __construct(UserPasswordEncoderInterface $encoder, ValidatorInterface $validator)
{
$this->encoder = $encoder;
$this->validator = $validator;
}
/**
* Return the URL to the login page.
*
* #return string
*/
protected function getLoginUrl()
{
return '/login';
}
/**
* Does the authenticator support the given Request?
*
* If this returns false, the authenticator will be skipped.
*
* #param Request $request
*
* #return bool
*/
public function supports(Request $request)
{
return true;
}
/**
*
* #param Request $request
*
* #return mixed Any non-null value
*
* #throws \UnexpectedValueException If null is returned
*/
public function getCredentials(Request $request)
{
$violations = $this->validator->validate($request->request->get('g-recaptcha-response'), new IsTrue());
if($violations->count() > 0){
throw new AuthenticationException(self::INVALID_RECAPTCHA);
}
return array(
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
);
}
/**
* Return a UserInterface object based on the credentials.
*
* The *credentials* are the return value from getCredentials()
*
* You may throw an AuthenticationException if you wish. If you return
* null, then a UsernameNotFoundException is thrown for you.
*
* #param mixed $credentials
* #param UserProviderInterface $userProvider
*
* #throws AuthenticationException
*
* #return UserInterface|null
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUsername($credentials['username']);
}
/**
* Returns true if the credentials are valid.
*
* If any value other than true is returned, authentication will
* fail. You may also throw an AuthenticationException if you wish
* to cause authentication to fail.
*
* The *credentials* are the return value from getCredentials()
*
* #param mixed $credentials
* #param UserInterface $user
*
* #return bool
*
* #throws AuthenticationException
*/
public function checkCredentials($credentials, UserInterface $user)
{
$plainPassword = $credentials['password'];
if (!empty($plainPassword) && !$this->encoder->isPasswordValid($user, $plainPassword)) {
throw new BadCredentialsException();
}
return true;
}
/**
* Called when authentication executed and was successful!
*
* If you return null, the current request will continue, and the user
* will be authenticated. This makes sense, for example, with an API.
*
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey The provider (i.e. firewall) key
*
* #return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
}
Make sure your authenticator is registered as a service. If you're using the default services.yaml configuration, that happens automatically. So you will be able also to pass the EWZRecaptchaBundle validator in the constructor of the GuardAuthenticator, then you can use it to validate the recaptcha value before sending the username and the password in getCredentials
and change security.yaml like this:
providers:
default:
entity:
class: App:User
property: phone
main:
pattern: ^/
anonymous: ~
provider: default
guard:
authenticators:
- App\Security\FormLoginAuthenticator
logout: ~
Related
i need to add an extra field to the json login, currently i can POST a _username and _password to my login_check endpoint but i also need to send a _school_name so the same username can be used in different schools.
I am using the json_login (https://symfony.com/doc/current/security/json_login_setup.html) with the lexik jwt bundle. Should i create a custom controller for this or a GuardAuthenticator?
I tried extending the AbstractGuardAuthenticator and i tried the AbstractFormLoginAuthenticator but they are both not working for me. By default i used this:
login:
pattern: ^/api/v1/token
stateless: true
anonymous: true
user_checker: App\Security\UserChecker
json_login:
check_path: /api/v1/token/login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
Then i added my custom Guard:
login:
pattern: ^/api/v1/token
stateless: true
anonymous: true
user_checker: App\Security\UserChecker
guard:
authenticators:
- App\Security\BaseAuthenticator
<?php
namespace App\Security;
use App\Entity\Client;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class BaseAuthenticator extends AbstractGuardAuthenticator
{
const LOGIN_ROUTE = 'login_check';
private EntityManagerInterface $entityManager;
private UrlGeneratorInterface $urlGenerator;
private CsrfTokenManagerInterface $csrfTokenManager;
private UserPasswordEncoderInterface $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
/**
* #param Request $request
* #return bool
*/
public function supports(Request $request)
{
if ($request->attributes->get('_route') !== static::LOGIN_ROUTE) {
return false;
}
if (!$request->isMethod(Request::METHOD_POST)) {
return false;
}
return true;
}
/**
* #param Request $request
* #return mixed
*/
public function getCredentials(Request $request)
{
return [
'client_name' => $request->request->get('client_name'),
'username' => $request->request->get('username'),
'password' => $request->request->get('password')
];
}
/**
* #param mixed $credentials
* #param UserProviderInterface $userProvider
* #return UserInterface|null
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$userRepository = $this->entityManager->getRepository(User::class);
$clientRepository = $this->entityManager->getRepository(Client::class);
$client = $clientRepository->findOneBy([
'name' => $credentials['client_name']
]);
if (!$client instanceof Client) {
throw new CustomUserMessageAuthenticationException('Client not found');
}
$user = $userRepository->findOneBy([
'client_id' => $client->getId(),
'username' => $credentials['username']
]);
if (!$user instanceof User) {
throw new CustomUserMessageAuthenticationException('User not found');
}
return $user;
}
/**
* #param mixed $credentials
* #param UserInterface $user
* #return bool
*/
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey
* #return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
return null;
}
/**
* #param Request $request
* #param AuthenticationException|null $authException
* #return Response
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
/**
* #param Request $request
* #param AuthenticationException $exception
* #return JsonResponse
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
// you may want to customize or obfuscate the message first
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
/**
* #return bool
*/
public function supportsRememberMe()
{
return false;
}
}
Thanks!
I've got a symfony 4 application developed in XAMPP. Now i made a setup of LAMPP in Unbuntu a few weeks ago. Now the first time i've installed my symfony project on Ubuntu with file rights 775 it doesn't work. In browser i get the message "Not Found - The requested URL was not found on this server." And the only thing i can find in my (error) logs is this:
[2020-04-19T22:56:51.564711+02:00] request.INFO: Matched route "homepage". {"route":"homepage","route_parameters":{"_route":"homepage","path":"/login","permanent":false,"keepQueryParams":true,"_controller":"Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction"},"request_uri":"http://192.168.0.100:8082/","method":"GET"} []
[2020-04-19T22:56:51.569203+02:00] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
[2020-04-19T22:56:51.569331+02:00] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} []
[2020-04-19T22:56:51.569400+02:00] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} []
And i don't know why it doesn't work. Can you help me?
Here is the Guard Authenticator Code:
<?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\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\Encoder\UserPasswordEncoderInterface;
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;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Authenticator for login form
*
*/
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
private $translator;
/**
* LoginFormAuthenticator constructor
*
* #param EntityManagerInterface $entityManager entity manager
* #param UrlGeneratorInterface $urlGenerator url generator
* #param CsrfTokenManagerInterface $csrfTokenManager csrf token manager
* #param UserPasswordEncoderInterface $passwordEncoder password encoder
* #param TranslatorInterface $translator translator
*/
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder, TranslatorInterface $translator)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
$this->translator = $translator;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request.
*
* #param Request $request request
*
* #return boolean Returning false will cause this authenticator to be skipped.
*/
public function supports(Request $request)
{
return 'login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
/**
* Get credentials
*
* #param Request $request request
*
* #return Array Returns with credentials array
*/
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('login')["email"],
'password' => $request->request->get('login')["password"],
'csrf_token' => $request->request->get('login')["_token"],
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
/**
* Get user
*
* #param Array $credentials credentials
* #param UserInterface $user user
*
* #return boolean returns true if password is valid. Otherwise false.
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
//throw new CustomUserMessageAuthenticationException($this->translator->trans('security.login.csrf', [], 'security'));
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findUser($credentials['email']);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException($this->translator->trans('general.error.emailOrUsername.not.found', [], 'messages'));
}
else {
if(!$user->getActive()) {
throw new CustomUserMessageAuthenticationException($this->translator->trans('general.error.emailOrUsername.not.active', [], 'messages'));
}
elseif(!$user->getAccepted()) {
throw new CustomUserMessageAuthenticationException($this->translator->trans('general.error.emailOrUsername.not.accepted.terms', [], 'messages'));
}
}
return $user;
}
/**
* Check credentials
*
* #param Array $credentials credentials
* #param UserInterface $user user
*
* #return boolean returns true if password is valid. Otherwise false.
*/
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* Override to change what happens after a correct username/password is submitted.
*
* #param Request $request request
* #param TokenInterface $token token
* #param String $providerKey provider key
*
* #return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if(!in_array($request->attributes->get('_route'), ['register'])) {
$user = $token->getUser();
$user->setLastLogin(new \DateTime());
$this->entityManager->persist($token->getUser());
$this->entityManager->flush();
}
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('admin_dashboard'));
}
/**
* get login url
*
* #return string returns url
*/
protected function getLoginUrl()
{
return $this->urlGenerator->generate('login');
}
}
I found my problem. In Xampp my symfony project works without .htaccess in /public directory. In LAMPP i need .htaccess in /public directory.
Therefore i installed composer require symfony/apache-pack in my symfony project with clicking y(es) during the installation. Now it works.
I'm trying to run a simple auth test using PHPUnit with Symfony 4 to see if the user has successfully logged in. I followed the documentation for running a auth test. The /admin page will automatically redirect to /login if the user is not signed in.
I'm getting the this error on running the test:
There was 1 failure:
1) App\Tests\AuthTest::testAdminPanel
Failed asserting that 302 is identical to 200.
~config/packages/test/security.yaml:
security:
firewalls:
main:
http_basic: ~
Test:
public function testAdminPanel() {
$client = static::createClient(array(), array(
'PHP_AUTH_USER' => 'username',
'PHP_AUTH_PW' => 'password',
));
$crawler = $client->request('GET', '/admin');
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
}
Note: The username account along with the password, password, do exist in the database.
Security Controller:
<?php
namespace App\Security;
use App\Form\LoginFormType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
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\Encoder\UserPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
/**
* Class LoginFormAuthenticator
* #package App\Security
*/
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
/**
* #var FormFactoryInterface
*/
private $formFactory;
/**
* #var EntityManager
*/
private $em;
/**
* #var RouterInterface
*/
private $router;
/**
* #var UserPasswordEncoder
*/
private $userPasswordEncoder;
/**
* LoginFormAuthenticator constructor.
* #param FormFactoryInterface $formFactory
* #param EntityManagerInterface $em
* #param RouterInterface $router
* #param UserPasswordEncoderInterface $userPasswordEncoder
*/
public function __construct(FormFactoryInterface $formFactory, EntityManagerInterface $em, RouterInterface $router, UserPasswordEncoderInterface $userPasswordEncoder)
{
$this->formFactory = $formFactory;
$this->em = $em;
$this->router = $router;
$this->userPasswordEncoder = $userPasswordEncoder;
}
/**
* #return string
*/
protected function getLoginUrl()
{
return $this->router->generate('login');
}
/**
* #param Request $request
* #return bool
*
* Called on every request to decide if this authenticator should be
* used for the request. Returning false will cause this authenticator
* to be skipped. Current implementation checks that this request method is POST and
* that the user is on the login page.
*
*/
public function supports(Request $request)
{
return ($request->attributes->get('_route') === 'login' && $request->isMethod('POST'));
}
/**
* #param Request $request
* #param AuthenticationException|null $authException
* #return RedirectResponse|Response
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse($this->router->generate('login'));
}
/**
* #param Request $request
* #return bool|mixed
*/
public function getCredentials(Request $request)
{
$form = $this->formFactory->create(LoginFormType::class);
$form->handleRequest($request);
$data = $form->getData();
$request->getSession()->set(
Security::LAST_USERNAME,
$data['username']
);
return $data;
}
/**
* #param mixed $credentials
* #param UserProviderInterface $userProvider
* #return null|object|UserInterface
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
$user = $this->em->getRepository('App:User')->findOneBy(['username' => $username]);
if(empty($user)) {
$user = $this->em->getRepository('App:User')->findOneBy(array('email' => $username));
}
return $user;
}
/**
* #param mixed $credentials
* #param UserInterface $user
* #return bool
*/
public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['password'];
if($this->userPasswordEncoder->isPasswordValid($user, $password)) {
return true;
}
return false;
}
/**
* #return string
*/
public function getDefaultSuccessRedirectUrl()
{
return $this->router->generate('default');
}
/**
* #param Request $request
* #param AuthenticationException $exception
* #return RedirectResponse
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->router->generate('login'));
}
/**
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey
* #return null|RedirectResponse|Response
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$user = $token->getUser();
$user->setLastLogin(new \DateTime('now'));
$this->em->persist($user);
$this->em->flush();
return new RedirectResponse($this->router->generate('easyadmin'));
}
/**
* #return bool
*/
public function supportsRememberMe()
{
return false;
}
}
You are using AbstractGuardAuthenticator class to authenticate your users. Following the link you provided, you have to use PostAuthenticationGuardToken class.
public function testAdminPanel(): void
{
// Create client
$client = static::createClient();
// Get user
$user = ...; // The user entity
// Login the user
$this->simulateLogin($user);
// Perform request
$crawler = $client->request('GET', '/admin');
// Assert
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
}
private function simulateLogin(User $user): void
{
// Get session
$session = self::$container->get('session');
// Set firewall
$firewall = '...'; // Your firewall name
// Authenticate the user
$token = new PostAuthenticationGuardToken($user, $firewall, $user->getRoles());
$session->set('_security_' . $firewall, serialize($token));
$session->save();
// Set cookie
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
To be able to use this method, you can create a fixture to create the user in your test database. See Doctrine Fixture Bundle and even use Liip Test Fixture Bundle to be able to load the fixture and truncate the database at each functional test.
I am using Symfony 5, but I guess it will not be very different.
Hope it will help !
your test seems fine. HTTP 302 means redirect, we are usually redirected after a successfull login.
try to add $client->followRedirects();
as suggested at https://symfony.com/doc/current/testing#redirecting
I am using FOSUserBundle in Symfone 2.8 webapp project. Currently the user is simply redirected to the homepage when he logs out. This should be changed to a "personal" logout page that can (optionally) display personal information (e.g. reminders for upcoming tasks or simple "Goodbey USERNAME" instead of just "Goodbey")...
So I need to access/use details of the currently logged out user. But since the user has just been logged out, I cannot access the user object any more?
How to solve this?
This is the configuration I use:
// config
security:
...
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
main:
...
logout:
path: fos_user_security_logout
target: /logoutpage
// route
<route id="user_logout" path="/logoutpage" methods="GET">
<default key="_controller">AppBundle:Default:logout</default>
</route>
// Controller action
public function logoutAction() {
$loggedOutUser = HOW_TO_GET_USER(???);
$template = 'AppBundle:Default:logout.html.twig';
return $this->render($template, array('user' => $loggedOutUser));
}
The clean way would be to save the User's name/data in the session within an EventSubscriber/Listener that listens for a security.interactive_logout event.
The 2 problems arising thereby would be:
there is no logout event dispatched by the default LogoutHandler
symfony clears the session on logout per default configuration
You can change the session-clearing behavior by setting invalidate_session to false
security:
firewalls:
main:
# [..]
logout:
path: 'fos_user_security_logout'
target: '/logoutpage'
invalidate_session: false # <- do not clear the session
handlers:
- 'Namespace\Bridge\Symfony\Security\Handler\DispatchingLogoutHandler'
For the logout event you can create a logout handler like this:
class DispatchingLogoutHandler implements LogoutHandlerInterface
{
/** #var EventDispatcherInterface */
protected $eventDispatcher;
/**
* #param EventDispatcherInterface $event_dispatcher
*/
public function __construct(EventDispatcherInterface $event_dispatcher)
{
$this->eventDispatcher = $event_dispatcher;
}
/**
* {#inheritdoc}
*/
public function logout(Request $request, Response $response, TokenInterface $token)
{
$this->eventDispatcher->dispatch(
SecurityExtraEvents::INTERACTIVE_LOGOUT,
new InteractiveLogoutEvent($request, $response, $token)
);
}
}
Add some service configuration (or use autowiring):
Namespace\Bridge\Symfony\Security\Handler\DispatchingLogoutHandler:
class: 'Namespace\Bridge\Symfony\Security\Handler\DispatchingLogoutHandler'
arguments:
- '#event_dispatcher'
Events class
namespace Namespace\Bridge\Symfony;
final class SecurityExtraEvents
{
/**
* #Event("\Namespace\Bridge\Symfony\Security\Event\Logout\InteractiveLogoutEvent")
*/
const INTERACTIVE_LOGOUT = 'security.interactive_logout';
}
Event itself:
final class InteractiveLogoutEvent extends Event
{
/**
* #var Request
*/
protected $request;
/**
* #var Response
*/
protected $response;
/**
* #var TokenInterface
*/
protected $token;
/**
* #param Request $request
* #param Response $response
* #param TokenInterface $token
*/
public function __construct(Request $request, Response $response, TokenInterface $token)
{
$this->request = $request;
$this->response = $response;
$this->token = $token;
}
/**
* #return TokenInterface
*/
public function getToken()
{
return $this->token;
}
/**
* #return TokenInterface
*/
public function getRequest()
{
return $this->token;
}
/**
* #return Response
*/
public function getResponse()
{
return $this->response;
}
/**
* #return string
*/
public function getName()
{
return SecurityExtraEvents::INTERACTIVE_LOGOUT;
}
}
And the subscriber:
class UserEventSubscriber implements EventSubscriberInterface
{
/** #var LoggerInterface */
protected $logger;
/** #param LoggerInterface $logger */
public function __construct(LoggerInterface $logger)
{
// inject the session here
$this->logger = $logger;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
SecurityExtraEvents::INTERACTIVE_LOGOUT => 'onInteractiveLogout',
);
}
/**
* {#inheritdoc}
*/
public function onInteractiveLogout(InteractiveLogoutEvent $event)
{
$user = $event->getToken()->getUser();
// save the username in the session here
$this->logger->info(
'A User has logged out.',
array(
'event' => SecurityExtraEvents::INTERACTIVE_LOGOUT,
'user' => array(
'id' => $user->getId(),
'email' => $user->getEmail(),
)
)
);
}
}
Enable the subscriber by tagging it with kernel.event_subscriber
Namespace\EventSubscriber\UserEventSubscriber:
class: 'Namespace\EventSubscriber\UserEventSubscriber'
arguments: ['#monolog.logger.user']
tags:
- { name: 'kernel.event_subscriber' }
Easy huh? A somewhat dirty solution would be creating a request listener that saves the username in the session-flashbag on every request so you can get it from there in the logout-page template.
I am trying to set up switch_user functionality on an application which authenticates using Apache's auth_kerb. REMOTE_USER is returned correctly and am able to log in. However when I try to masquerade as a different user I am unable to. The user I wish to switch to does exist within the application. The attempt to switch user occurs but pre authentication is called again and the initial REMOTE_USER is loaded.
Any ideas on how to get switch_user working using remote_user and custom user provider?
security.yml
security:
providers:
webservice_user_provider:
id: webservice_user_provider
...
firewalls:
secured_area:
switch_user: { role: ROLE_ALLOWED_TO_SWITCH, parameter: _masquerade }
pattern: ^/
remote_user:
provider: webservice_user_provider
...
services.yml
parameters:
account.security_listener.class: Acme\MyUserBundle\Listener\SecurityListener
services:
account.security_listener:
class: %account.security_listener.class%
arguments: ['#security.authorization_checker', '#session', '#doctrine.orm.entity_manager', '#router', '#event_dispatcher']
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
- { name: kernel.event_listener, event: security.switch_user, method: onSecuritySwitchUser }
webservice_user_provider:
class: Acme\MyUserBundle\Security\User\WebserviceUserProvider
calls:
- [setEntityManager , ['#logger', '#doctrine.orm.entity_manager']]
...
SecurityListener.php
namespace Acme\MyUserBundle\Listener;
use ...
/**
* Class SecurityListener
* #package Acme\MyUserBundle\Listener
*/
class SecurityListener {
protected $session;
protected $security;
protected $em;
protected $router;
protected $dispatcher;
public function __construct(
AuthorizationCheckerInterface $security,
Session $session,
EntityManager $em,
UrlGeneratorInterface $router,
EventDispatcherInterface $dispatcher
// TraceableEventDispatcher $dispatcher
// ContainerAwareEventDispatcher $dispatcher
) {
$this->security = $security;
$this->session = $session;
$this->em = $em;
$this->router = $router;
$this->dispatcher = $dispatcher;
}
/**
*
* #param AuthenticationFailureEvent $event
* #throws AuthenticationException
*/
public function onAuthenticationFailure(AuthenticationFailureEvent $event) {
throw new AuthenticationException($event->getAuthenticationException());
}
/**
* #param InteractiveLoginEvent $event
*/
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
// set some defaults...
}
/**
* #param SwitchUserEvent $event
*/
public function onSecuritySwitchUser(SwitchUserEvent $event) {
$this->dispatcher->addListener(KernelEvents::RESPONSE, array($this, 'onSwitchUserResponse'));
}
/**
* #param FilterResponseEvent $event
*/
public function onSwitchUserResponse(FilterResponseEvent $event) {
$response = new RedirectResponse($this->router->generate('acme_mybundle_default_index'));
$event->setResponse($response);
}
}
WebServiceProvider.php
namespace Acme\MyUserBundle\Security\User;
use ...
class WebserviceUserProvider implements UserProviderInterface {
protected $entityManager;
protected $logger;
/**
*
* #param LoggerInterface $logger
* #param EntityManager $em
*/
public function setEntityManager(LoggerInterface $logger, EntityManager $em) {
$this->logger = $logger;
$this->entityManager = $em;
}
/**
*
* #param string $username
* #return Person
* #throws UsernameNotFoundException
*/
public function loadUserByUsername($username) {
# Find the person
$person = $this->entityManager->getRepository('AcmeMyBundle:Person')
->find($username);
if ($person) {
$this->logger->debug("Logged in, finding person: " . $person->getUsername());
return $person;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
/**
*
* #param \Symfony\Component\Security\Core\User\UserInterface $person
* #throws \Symfony\Component\Security\Core\Exception\UnsupportedUserException
* #internal param \Symfony\Component\Security\Core\User\UserInterface $user
* #return Person
*/
public function refreshUser(UserInterface $person) {
if (!$person instanceof Person) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($person))
);
}
return $this->loadUserByUsername($person->getUsername());
}
/**
*
* #param type $class
* #return type
*/
public function supportsClass($class) {
return $class === 'Acme\MyBundle\Entity\Person';
}
}
This fix involves adapting AbstractPreAuthenticatedListener to check for the existence of the standard token that matches the logged in user, and if not a customised token that has stored the logged in user, but is attached to the 'switched to' userid.
This is the important (non copied) part of the code:
if (null !== $token = $this->securityContext->getToken()) {
if ($token instanceof PreAuthenticatedToken && $this->providerKey == $token->getProviderKey() && $token->isAuthenticated() && $token->getUsername() === $user) {
return;
}
// Switch user token. Check the original token.
if ($token instanceof PreAuthenticatedSwitchUserToken && $this->providerKey == $token->getProviderKey() && $token->isAuthenticated() && $token->getOriginalUsername() === $user) {
return;
}
}
The token stores the logged in user and returns it with getOriginalUsername.
Store the existing authentication data (passed in $preAuthenticatedData)
/**
* Constructor.
*/
public function __construct($user, $credentials, $providerKey, array $roles = array(), $preAuthenticatedData)
{
parent::__construct($roles);
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->setUser($user);
$this->credentials = $credentials;
$this->providerKey = $providerKey;
if (!is_array($preAuthenticatedData) && count($preAuthenticatedData) > 0) {
throw new \InvalidArgumentException('No preauthenticated data. Must have the server login credentials.');
}
$this->original_username = $preAuthenticatedData[0];
if ($roles) {
$this->setAuthenticated(true);
}
}
Getter
public function getOriginalUsername() {
return $this->original_username;
}
Stash changes
/**
* {#inheritdoc}
*/
public function serialize()
{
return serialize(array($this->credentials, $this->providerKey, $this->original_username, parent::serialize()));
}
/**
* {#inheritdoc}
*/
public function unserialize($str)
{
list($this->credentials, $this->providerKey, $this->original_username, $parentStr) = unserialize($str);
parent::unserialize($parentStr);
}
These changes fit into the context of broader customisation of the Symfony security system. The source code for this is in github.
1 services.yml
Set account.security_listener, security.authentication.switchuser_listener and security.authentication.listener.remote_user_switch
This is in addition to the expected user provider.
2 security.yml
Use this security provider
secured_area:
switch_user: { role: ROLE_ALLOWED_TO_SWITCH, parameter: _masquerade }
pattern: ^/
remote_user_switch:
provider: webservice_user_provider
3 Check that the user provider loads the backing data for your user.
4 Install security files.
RemoteUserSwitchFactory.php: defines the listener to handle the
authentication events.
PreAuthenticatedWithSwitchUserListener.php:
our special authentication logic. SwitchUserListener.php: handles
the switch user event.
PreAuthenticatedSwitchUserToken.php: token to
store the logged in user as secondary data.
WebserviceUser.php: our
user data entity
WebserviceUserProvider.php: queries for user data.