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.
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'm implementing a login system with guard authentication in my symfony application. I've already started to implement the system but I must be doing something incorrectly.
I will start by showing what I have implemented and in the end explain what is happening ...
security.yml
security:
encoders:
UserBundle\Entity\User: bcrypt
providers:
custom_own_provider:
entity:
class: UserBundle:User
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
custom:
pattern: ^/ad/
anonymous: ~
provider: custom_own_provider
remember_me:
name: 'nothing'
secure: true
httponly: true
secret: '%secret%'
lifetime: 604800 # 1 week in seconds
path: /
domain: ~
guard:
authenticators:
- app.authenticator.form
services.yml
services:
app.authenticator.form:
class: UserBundle\Security\LoginFormAuthenticator
autowire: true
arguments: ["#service_container"]
LoginController
/**
* #Route("/login", name="login")
*/
public function loginAction(Request $request) {
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'AppBundle:login:index.html.twig',
[
'error' => $error ? $error->getMessage() : NULL,
'last_username' => $lastUsername
]
);
}
Public controller with homepage: After login success, the user is redirected here. It happens that here, when validated if the user is authenticated I am unsuccessful. the 'getuser' from the security token is returning 'anon'.
/**
* #Route("/", name="homepage")
*/
public function publicHomepageAction (){
// DEBUG: This method gets a user from the Security Token Storage. The user here comes as 'anon'.
$user = $this->getUser();
// If user is already logged show internal homepage
$securityContext = $this->container->get('security.authorization_checker');
if($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')
|| $securityContext->isGranted('IS_AUTHENTICATED_FULLY')
){
// Code if user is authenticated
...
}
return $this->render('splash_page/homepage.html.twig');
}
And finally, my FormAuthenticator:
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
private $container;
/**
* Default message for authentication failure.
*
* #var string
*/
private $failMessage = 'Invalid credentials';
/**
* Creates a new instance of FormAuthenticator
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* {#inheritdoc}
*/
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return array(
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
);
}
/**
* {#inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$email = $credentials['email'];
return $userProvider->loadUserByUsername($email);
}
/**
* {#inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
$plainPassword = $credentials['password'];
$encoder = $this->container->get('security.password_encoder');
if (!$encoder->isPasswordValid($user, $plainPassword)) {
throw new CustomUserMessageAuthenticationException($this->failMessage);
}
return true;
}
/**
* {#inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->container->get('router')->generate('homepage');
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->container->get('router')->generate('login');
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->container->get('router')->generate('login');
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function supportsRememberMe()
{
return true;
}
/**
* 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 $request->request->has('_username') && $request->request->has('_password');
}
}
My User Entity implements UserInterface.
Although on the homepage it indicates to me that I am authenticated as 'anon', if I try to login again, through the debug, I noticed that it detects that I am authenticated and redirects to the homepage controller but in this controller it says that I am like anon and back to redirect to the splash page.
It gives the feeling that somewhere between validation and redirection to the controller loses token storage.
Any idea?
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: ~
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.
I have a symfony app which serve a RESTful API(for mobile app) and have backend administration.
I can succesfuly login to the backend via facebook, but how should I allow loggin via the RESTful API?
Wooh.. after almost 12 hours(!) here is the solution for anyone who looking for too:
We need to create new custom firewall
This factory should connect to the FOSFacebook and validate the token
If it using our new firewall it should manually disable any session or cookie.
To use the firewall we need to send our token in every request
The code
First define our firewall listener
GoDisco/UserBundle/Security/Firewall/ApiFacebookListener.php
<?php
/**
* Authored by AlmogBaku
* almog.baku#gmail.com
* http://www.almogbaku.com/
*
* 9/6/13 2:17 PM
*/
namespace Godisco\UserBundle\Security\Firewall;
use FOS\FacebookBundle\Security\Authentication\Token\FacebookUserToken;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
/**
* API gateway through Facebook oAuth token: Firewall
*
* Class ApiFacebookListener
* #package Godisco\UserBundle\Security\Firewall
*/
class ApiFacebookListener implements ListenerInterface
{
/**
* #var \Symfony\Component\Security\Core\SecurityContextInterface
*/
protected $securityContext;
/**
* #var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
*/
protected $authenticationManager;
/**
* #var Session
*/
protected $session;
/**
* #var string
*/
protected $providerKey;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, Session $session, $providerKey)
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->session = $session;
$this->providerKey=$providerKey;
}
/**
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event The event.
*/
public function handle(GetResponseEvent $event)
{
$accessToken = $event->getRequest()->get('access_token');
$token = new FacebookUserToken($this->providerKey, '', array(), $accessToken);
/**
* force always sending token
*/
$_COOKIE=array();
$this->session->clear();
try {
if($accessToken)
$returnValue = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($returnValue);
}
} catch(AuthenticationException $exception) {
if(!empty($accessToken))
$event->setResponse(new Response(array("error"=>$exception->getMessage()),401));
}
}
}
Than create a new security factory which calling our listener, and will connect the authentication to the FOSFacebookBundle.
GoDisco/UserBundle/DependencyInjection/Security/Factory/ApiFacebookFactory.php
<?php
/**
* Authored by AlmogBaku
* almog.baku#gmail.com
* http://www.almogbaku.com/
*
* 9/6/13 2:31 PM
*/
namespace GoDisco\UserBundle\DependencyInjection\Security\Factory;
use FOS\FacebookBundle\DependencyInjection\Security\Factory\FacebookFactory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
/**
* API gateway through Facebook oAuth token: Factory
*
* Class ApiFacebookFactory
* #package GoDisco\UserBundle\DependencyInjection\Security\Factory
*/
class ApiFacebookFactory extends FacebookFactory
{
/**
* {#inheritdoc}
*/
public function getKey()
{
return 'api_facebook';
}
/**
* {#inheritdoc}
*/
public function addConfiguration(NodeDefinition $node)
{
$builder = $node->children();
$builder
->scalarNode('provider')->end()
->booleanNode('remember_me')->defaultFalse()->end()
;
foreach ($this->options as $name => $default) {
if (is_bool($default)) {
$builder->booleanNode($name)->defaultValue($default);
} else {
$builder->scalarNode($name)->defaultValue($default);
}
}
}
/**
* {#inheritdoc}
*/
protected function createEntryPoint($container, $id, $config, $defaultEntryPointId)
{
return null;
}
/**
* {#inheritdoc}
*/
protected function createListener($container, $id, $config, $userProvider)
{
$listenerId = "api_facebook.security.authentication.listener";
$listener = new DefinitionDecorator($listenerId);
$listener->replaceArgument(3, $id);
$listenerId .= '.'.$id;
$container->setDefinition($listenerId, $listener);
return $listenerId;
}
}
Defining the listener service, so we can inject the arguments
GoDisco/UserBundle/Resources/config/services.yml
services:
api_facebook.security.authentication.listener:
class: GoDisco\UserBundle\Security\Firewall\ApiFacebookListener
arguments: ['#security.context', '#security.authentication.manager', '#session', '']
Defining our new firewall!
app/config/security.yml
security:
api:
pattern: ^/api
api_facebook:
provider: godisco_facebook_provider
stateless: true
anonymous: true
main:
...
You need to implement oAuth authentication from your client app.
This was answered before:
How to restfully login, Symfony2 Security, FOSUserBundle, FOSRestBundle?