Symfony - Authentication with an API Token - Request token user is null - php

For the record, I'm using PHP 7.0.0, in a Vagrant Box, with PHPStorm. Oh, and Symfony 3.
I'm following the API Key Authentication documentation. My goal is:
To allow the user to provide a key as a GET apiKey parameter to authenticate for any route, except the developer profiler etc obviously
To allow the developer to write $request->getUser() in a controller to get the currently logged in user
My problem is that, although I believe I've followed the documentation to the letter, I'm still getting a null for $request->getUser() in the controller.
Note: I've removed error checking to keep the code short
ApiKeyAuthenticator.php
The thing that processes the part of the request to grab the API key from it. It can be a header or anything, but I'm sticking with apiKey from GET.
Differences from documentation, pretty much 0 apart from that I'm trying to keep the user authenticated in the session following this part of the docs.
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->query->get('apiKey');
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
// The part where we try and keep the user in the session!
$user = $token->getUser();
if ($user instanceof ApiKeyUser) {
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
}
ApiKeyUserProvider.php
The custom user provider to load a user object from wherever it can be loaded from - I'm sticking with the default DB implementation.
Differences: only the fact that I have to inject the repository into the constructor to make calls to the DB, as the docs allude to but don't show, and also returning $user in refreshUser().
class ApiKeyUserProvider implements UserProviderInterface
{
protected $repo;
// I'm injecting the Repo here (docs don't help with this)
public function __construct(UserRepository $repo)
{
$this->repo = $repo;
}
public function getUsernameForApiKey($apiKey)
{
$data = $this->repo->findUsernameByApiKey($apiKey);
$username = (!is_null($data)) ? $data->getUsername() : null;
return $username;
}
public function loadUserByUsername($username)
{
return $this->repo->findOneBy(['username' => $username]);
}
public function refreshUser(UserInterface $user)
{
// docs state to return here if we don't want stateless
return $user;
}
public function supportsClass($class)
{
return 'Symfony\Component\Security\Core\User\User' === $class;
}
}
ApiKeyUser.php
This is my custom user object.
The only difference I have here is that it contains doctrine annotations (removed for your sanity) and a custom field for the token. Also, I removed \Serializable as it didn't seem to be doing anything and apparently Symfony only needs the $id value to recreate the user which it can do itself.
class ApiKeyUser implements UserInterface
{
private $id;
private $username;
private $password;
private $email;
private $salt;
private $apiKey;
private $isActive;
public function __construct($username, $password, $salt, $apiKey, $isActive = true)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->apiKey = $apiKey;
$this->isActive = $isActive;
}
//-- SNIP getters --//
}
security.yml
# Here is my custom user provider class from above
providers:
api_key_user_provider:
id: api_key_user_provider
firewalls:
# Authentication disabled for dev (default settings)
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# My new settings, with stateless set to false
secured_area:
pattern: ^/
stateless: false
simple_preauth:
authenticator: apikey_authenticator
provider:
api_key_user_provider
services.yml
Obviously I need to be able to inject the repository into the provider.
api_key_user_repository:
class: Doctrine\ORM\EntityRepository
factory: ["#doctrine.orm.entity_manager", getRepository]
arguments: [AppBundle\Security\ApiKeyUser]
api_key_user_provider:
class: AppBundle\Security\ApiKeyUserProvider
factory_service: doctrine.orm.default_entity_manager
factory_method: getRepository
arguments: ["#api_key_user_repository"]
apikey_authenticator:
class: AppBundle\Security\ApiKeyAuthenticator
public: false
Debugging. It's interesting to note that, in ApiKeyAuthenticator.php, the call to $user = $token->getUser(); in authenticateToken() always shows an anon. user, so it's clearly not being stored in the session.
Also note how at the bottom of the authenticator we do actually return a new PreAuthenticatedToken with a user found from the database:
So it's clearly found me and is returning what it's supposed to here, but the user call in the controller returns null. What am I doing wrong? Is it a failure to serialise into the session because of my custom user or something? I tried setting all the user properties to public as somewhere in the documentation suggested but that made no difference.

So it turns out that calling $request->getUser() in the controller doesn't actually return the currently authenticated user as I would have expected it to. This would make the most sense for this object API imho.
If you actually look at the code for Request::getUser(), it looks like this:
/**
* Returns the user.
*
* #return string|null
*/
public function getUser()
{
return $this->headers->get('PHP_AUTH_USER');
}
That's for HTTP Basic Auth! In order to get the currently logged in user, you need to do this every single time:
$this->get('security.token_storage')->getToken()->getUser();
This does, indeed, give me the currently logged in user. Hopefully the question above shows how to authenticate successfully by API token anyway.
Alternatively, don't call $this->get() as it's a service locator. Decouple yourself from the controller and inject the token service instead to get the token and user from it.

To get the currently logged in User inside your Controller simply call:
$this->getUser();
This will refer to a method in Symfony's ControllerTrait, which basically wraps the code provided in Jimbo's answer.
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
return $user;
}

Related

Symfony 5 ApiKeyAuthenticator with SelfValidatingPassport

I am working on a new Symfony 5.3.6 project and want to implement authentication, based on the new system as stated in:
https://symfony.com/doc/current/security/authenticator_manager.html#creating-a-custom-authenticator
I do not have any users and just want to check if the sent api token is correct, so when implementing this method:
public function authenticate(Request $request): PassportInterface
{
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null === $apiToken) {
// The token header was empty, authentication fails with HTTP Status Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(new UserBadge($apiToken));
}
where exactly is the checking done? Have i forgotten to implement another Class somewhere?
If I leave the code as is it lands directly in onAuthenticationFailure.
I understand, that I could implement Users/UserProvider with an attribute $apiToken and then the system would check if the database entry corresponds with the token in the request. But i do not have users.
It should be possible without having users, because on the above URL, it says:
Self Validating Passport
If you don’t need any credentials to be checked (e.g. when using API
tokens), you can use the
Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport.
This class only requires a UserBadge object and optionally Passport
Badges.
But that is a little thin. How do I "use" it?
Ok, I think I got the point, in any case, you need to handle some User & then you need to create a customer Userprovider.
Here my logic:
App\Security\UserProvider:
class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
public function loadUserByIdentifier($identifier): UserInterface
{
if ($identifier === 'YOUR_API_KEY') {
return new User();
}
throw new UserNotFoundException('API Key is not correct');
}
...
App\Security\ApiKeyAuthenticator:
class ApiKeyAuthenticator extends AbstractAuthenticator
{
private UserProvider $userProvider;
public function __construct(UserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
public function supports(Request $request): ?bool
{
// allow api docs page
return trim($request->getPathInfo(), '/') !== 'docs';
}
public function authenticate(Request $request): Passport
{
$apiToken = $request->headers->get('X-API-KEY');
if (null === $apiToken) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(
new UserBadge($apiToken, function () use ($apiToken) {
return $this->userProvider->loadUserByIdentifier($apiToken);
})
);
}
It works for me, my API is protected by a basic API Key in the header. I don't know if it's the best way, but seems ok.
And define in your security.yaml:
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
id: App\Security\UserProvider
You can use next validation
return new SelfValidatingPassport(
new UserBadge($apiToken, function() use ($apiToken) {
// TODO: here you can implement any check
})
);

How can I have optional services in the Symfony framework?

I would like to be able to get the current logged in user's credentials (email, password, etc) from the container. So, this is what I did:
security.token:
class: Symfony\Component\Security\Core\Authentication\Token\TokenInterface
factory: ["#security.token_storage", "getToken"]
private: true
security.current_user_credentials:
class: Symfony\Component\Security\Core\User\UserInterface
factory: ["#security.token", "getUser"]
security.current_user:
class: AppBundle\Entity\User
factory: ["#security.current_user_credentials", "getUser"]
When I do this and I'm logged in, it works fine. However, when I'm logged out, I get this in dev.log:
[2015-06-22 12:28:11] php.CRITICAL: Fatal Error: Call to a member function getUser() on string {"type":1,"file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":3107,"level":-1,"stack":[{"function":"getSecurity_CurrentUserService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":674,"args":[]},{"function":"getCommandHistoryCreatorService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1929,"args":[]},{"function":"lazyLoad","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1894,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":99,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":158,"args":[]},{"function":"getNotCalledListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php","line":48,"args":[]},{"function":"lateCollect","type":"->","class":"Symfony\\Component\\HttpKernel\\DataCollector\\EventDataCollector","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Profiler/Profiler.php","line":115,"args":[]},{"function":"saveProfile","type":"->","class":"Symfony\\Component\\HttpKernel\\Profiler\\Profiler","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php","line":146,"args":[]},{"function":"onKernelTerminate","type":"->","class":"Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php:61}","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"__invoke","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/app/cache/dev/classes.php:1824}","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"doDispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1757,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1918,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":124,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":3067,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\HttpKernel","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2409,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\Kernel","file":"/var/www/html/phoenix/web/app_dev.php","line":20,"args":[]},{"function":"{main}","file":"/var/www/html/phoenix/web/app_dev.php","line":0,"args":[]}]} []
Is it possible to make the security.current_user_credentials and security.current_user optional? Is this error caused by these services?
Recently I ran into a similar issue and if you try to access a route that does not exist you might see the same error. I was working on a task where I needed to get hold of logged in user in my service and this is how I achieved it
My services.yml
services:
student_application_subscriber:
class: namespace\YourBundle\EventListener\StudentApplicationSubscriber
arguments:
- #doctrine.orm.entity_manager
- #security.token_storage
- #security.authorization_checker
- #twig
This is my service class StudentApplicationSubscriber
namespace yournamespace\YourBundleBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class StudentApplicationSubscriber implements EventSubscriberInterface
{
protected $em;
protected $twig;
protected $tokenStorage;
protected $authChecker;
function __construct(EntityManager $em, $tokenStorage, $authChecker, $twig)
{
$this->em = $em;
$this->twig = $twig;
$this->tokenStorage = $tokenStorage;
$this->authChecker = $authChecker;
}
public static function getSubscribedEvents()
{
return array(
'kernel.request' => 'onKernelRequest'
);
}
public function onKernelRequest()
{
if (!$token = $this->tokenStorage->getToken()) {
return;
}
$user = $token->getUser();
if (!is_object($user)) {
// there is no user - the user may not be logged in
return;
}
//get details of logged in user
$get_user_details = $this->tokenStorage->getToken()->getUser();
//make sure to pull information when user is logged in
if ($this->authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
//get user id of logged in user
$userId = $get_user_details->getId();
//perform your logic here
}
}
}
What are you trying to achieve?
At first sight I would suggest would be either having a kernel listener that would check if there is a User and performe the required actions, or check that in your security.current_user_credentials.
I guess, if you just pass to your service and add below logic inside that function, then it work for both annon and authenticated users:
function dummyFunction($securityContext)) {
$email = $username = '';
if($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
$email = $securityContext->getToken()->getUser()->getEmail();
$username = $securityContext->getToken()->getUser()->getUsername();
}
..........................
}

Symfony2 Authentication (Login) without Doctrine?

i'm new # symfony and absolutely new at doctrine. On the symfony page is an tutorial about build an login process in sf2. And i love it, BUT i cant work with doctrine and i dont wanna work with it (there are many thinks that dont work with doctrine... - e.g. enum´s, etc).
How i create an login controller, firewall setup, etc.. is explained very good. BUT! i would like to create it without doctrine... i have an existing database, and i love plain sql. :-)
How can i use plain sql in an UserInterface... that will work with the build-in login from sf2?
Thx a lot...
What you want to do is to plug in your own UserProvider:
http://symfony.com/doc/current/cookbook/security/entity_provider.html
In place of the Doctrine 2 Object Relational Manager (ORM) you might consider using the Doctrine 2 Database Access Layer(DBAL). This is a thin sql layer built on top of PDO. Has some helper routines for building sql.
http://symfony.com/doc/current/cookbook/doctrine/dbal.html
Of course you can just use PDO directly:
http://symfony.com/doc/current/cookbook/configuration/pdo_session_storage.html
Here is a solution I propose on Symfony 3.0.
As I implement an existing postgresql database and I want to re-use a lot of an already existing PHP code, ORM was not a fit in my case (too much timecost to do the reverse engineering). Hence I have been actively searching how to use Symfony without ORM. (I did implement DBAL because what I wrote for PDO works with it... so far ... with some minor fixes).
Let's start:
The important concept I needed to understand were taken from this page:
http://symfony.com/doc/current/cookbook/security/custom_provider.html + all the things about the security.yml file.
About the security file, it took me a while to understand that the node 'providers' had only two native option:'entity' or 'memory'. And with 'entity', one was bound to work with ORM. So I figured out that I needed to develop my own provider that would work with regular PDO.
Here some part of my code to give you an idea on how I finally made it work, you'll find below the following files:
security.yml (There are probably still some room for improvement here, as I haven't dig too much the 'firewall' part organization);
CustomUsersProvider.php : The class implementing the interface
UserProviderInterface (The class PdoCustom and its method getUser() are my own PDO cooking to retrieve the user from my DB);
services.yml: The service giving access to the class CustomUsersProvider and hence providing access to the 'provider' in the security file (if I unsderstood it correctly), I pass the DBAL database config as an argument in the service so it can be used in the instance of my object CustomUsersProvider;
CustomUsers.php : a class acting as an entity of my table
'custom_users', to work with the PDO::FETCH_CLASS type of PDO fetch; it implements Symfony interfaces: UserInterface and EquatableInterface;
You still have to implement the Symfony Controller SecurityController (find it here: http://symfony.com/doc/2.0/book/security.html) and its route to /login and Twig file.
You'll probably notice when reading the code that $email is used as $username;
[projectname]\app\config\security.yml
encoders:
CustomBundle\Entity\CustomUsers:
algorithm: [enter an encoding algorithm, eg: MD5, sha512, etc.]
role_hierarchy:
ROLE_NAME2: ROLE_NAME1
ROLE_NAME3: ROLE_NAME2
providers:
custom_users_provider:
id: custom_users_provider
firewalls:
main_login:
pattern: ^/login$
anonymous: ~
main:
pattern: ^/
anonymous: true
provider: custom_users_provider
form_login:
check_path: /login_check
login_path: /login
provider: custom_users_provider
username_parameter: email
password_parameter: password
logout: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/secured_for_role_name, role: ROLE_NAME }
[projectname]\CustomBundle\Security\CustomUsersProvider.php
<?php
namespace CustomBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use CustomBundle\DependencyInjection\PdoCustom;
use CustomBundle\Entity\CustomUsers;
class CustomUsersProvider implements UserProviderInterface
{
private $dbalConnection;
private $logger;
public function __construct($dbalConnection){
$this->dbalConnection = $dbalConnection;
////////////////EASTER EGG//////////////////////////
$this->logger = $GLOBALS['kernel']->getContainer()->get('logger');
$logger->info('EASTER EGG: YOU CAN ADD LOG THAT CAN BE FOUND UNDER [projectname]\var\logs\dev.log WHEN LAUNCHING THRU app_dev.php, WICH COULD BE USEFUL TOO');
////////////////////////////////////////////////////
}
public function loadUserByUsername($username)
{
$PdoCustom = new PdoCustom($this->dbalConnection);
$userData = $PdoCustom->getUser($username);
if ($userData) {
$password = $userData->password;
$salt = null;
$role = $userData->role;
$resToReturn = new CustomUsers();
$resToReturn->setCharact($username, $password, $role);
return $resToReturn;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof CustomUsers) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'CustomBundle\Entity\CustomUsers';
}
}
?>
[projectname]\src\CustomBundle\Ressources\config\services.yml
custom_bundle.custom_users_provider.class : CustomBundle\Security\CustomUsersProvider
custom_users_provider:
class: %custom_bundle.custom_users_provider.class%
arguments: ["#doctrine.dbal.default_connection"]
[projectname]\CustomBundle\Entity\CustomUsers.php
<?php
namespace CustomBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
class CustomUsers implements UserInterface, EquatableInterface {
public $email;
public $password;
public $role;
public function __construct(){
}
public function setCharact($email,$password,$role){
$this->email = $email;
$this->password = $password;
$this->status = $status;
}
public function getRoles(){
return $this->role;
}
public function getPassword(){
return $this->password;
}
public function getSalt(){
return null;
}
public function getUsername(){
return $this->email;
}
public function eraseCredentials(){
}
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof CustomUsers) {
return false;
}
if ($this->user_password !== $user->getPassword()) {
return false;
}
//if ($this->salt !== $user->getSalt()) {
// return false;
//}
if ($this->email !== $user->getUsername()) {
return false;
}
return true;
}
}
?>
That's it, you still have some work to figure it all out by yourself but I hope that will give you directions that I had a really hard time to find on HOW TO USE SYMFONY WITHOUT ORM:))

Anonymous user object in symfony

I'm using the basic user login/logout system provided with Symfony and it works fine as long as people log in. In that case the $user object is always provided as needed.
The problem is then when logged out (or not lgged in yet) there is no user object. Is there a possibility to have (in that case) a default user object provided with my own default values?
Thanks for your suggestions
Because the solution mention above by #Chopchop (thanks anyway for your effort) didn't work here I wrote a little workaround.
I created a new class called myController which extends Controller. The only function i override is the getUser() function. There I implement it like this:
public function getUser()
{
$user = Controller::getUser();
if ( !is_object($user) )
{
$user = new \ACME\myBundle\Entity\User();
$user->setUserLASTNAME ('RaRa');
$user->setID (0);
// etc...
}
return $user;
}
This works fine for me now. The only problem is that you really have to be careful NOT to forget to replace Controller by myController in all your *Controller.php files. So, better suggestions still welcome.
Works in Symfony 3.3
Using the suggestion of #Sfblaauw, I came up with a solution that uses a CompilerPass.
AppBundle/AppBundle.php
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OverrideAnonymousUserCompilerPass());
}
}
OverrideAnonymousUserCompilerPass.php
class OverrideAnonymousCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.authentication.listener.anonymous');
$definition->setClass(AnonymousAuthenticationListener::class);
}
}
AnonymousAuthenticationListener.php
class AnonymousAuthenticationListener implements ListenerInterface
{
private $tokenStorage;
private $secret;
private $authenticationManager;
private $logger;
public function __construct(TokenStorageInterface $tokenStorage, $secret, LoggerInterface $logger = null, AuthenticationManagerInterface $authenticationManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->secret = $secret;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
}
public function handle(GetResponseEvent $event)
{
if (null !== $this->tokenStorage->getToken()) {
return;
}
try {
// This is the important line:
$token = new AnonymousToken($this->secret, new AnonymousUser(), array());
if (null !== $this->authenticationManager) {
$token = $this->authenticationManager->authenticate($token);
}
$this->tokenStorage->setToken($token);
if (null !== $this->logger) {
$this->logger->info('Populated the TokenStorage with an anonymous Token.');
}
} catch (AuthenticationException $failed) {
if (null !== $this->logger) {
$this->logger->info('Anonymous authentication failed.', array('exception' => $failed));
}
}
}
}
This file is a copy of the AnonymousAuthenticationListener that comes with Symfony, but with the AnonymousToken constructor changed to pass in an AnonymousUser class instead of a string. In my case, AnonymousUser is a class that extends my User object, but you can implement it however you like.
These changes mean that {{ app.user }} in Twig and UserInterface injections in PHP will always return a User: you can use isinstance to tell if it's an AnonymousUser, or add a method isLoggedIn to your User class which returns true in User but false in AnonymousUser.
you can redirect the user not authenticated and force a fake login (to create a user ANONYMOUS)
and set it as well on logout
public function logoutAction(){
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous');
$session = $this->getRequest()->getSession();
$session->set('user', $user);
}
and if user is not set
public function checkLoginAction(){
if(!$session->get('user')){
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous');
$session = $this->getRequest()->getSession();
$session->set('user', $user);
}
//this->redirect('/');
}
in you security.yml
security:
firewalls:
main:
access_denied_url: /check_login/
access_control:
- { path: ^/$, role: ROLE_USER }
This is only an example i haven't tested (and will probably don't, since i don't get the purpose of doing this:) )
Using Symfony 2.6
Like Gordon says use the authentication listener to override the default anonymous user.
Now you can add the properties that you need to the anonymous user, in my case the language and the currency.
security.yml
parameters:
security.authentication.listener.anonymous.class: AppBundle\Security\Http\Firewall\AnonymousAuthenticationListener
AnonymousAuthenticationListener.php
namespace AppBundle\Security\Http\Firewall;
...
use AppBundle\Security\User\AnonymousUser;
class AnonymousAuthenticationListener implements ListenerInterface
{
...
public function handle(GetResponseEvent $event)
{
...
try {
$token = new AnonymousToken($this->key, new AnonymousUser(), array());
...
}
}
}
AnonymousUser.php
class AnonymousUser implements UserInterface
{
public function getUsername() { return 'anon.'; }
}

How to use the AccessDecisionManager in Symfony2 for authorization of arbitrary users?

I'd like to be able to verify whether or not attributes (roles) are granted to any arbitrary object implementing UserInterface in Symfony2. Is this possible?
UserInterface->getRoles() is not suitable for my needs because it does not take the role hierarchy into account, and I'd rather not reinvent the wheel in that department, which is why I'd like to use the Access Decision Manager if possible.
Thanks.
In response to Olivier's solution below, here is my experience:
You can use the security.context service with the isGranted method. You can pass a second argument which is your object.
$user = new Core\Model\User();
var_dump($user->getRoles(), $this->get('security.context')->isGranted('ROLE_ADMIN', $user));
Output:
array (size=1)
0 => string 'ROLE_USER' (length=9)
boolean true
My role hierarchy:
role_hierarchy:
ROLE_USER: ~
ROLE_VERIFIED_USER: [ROLE_USER]
ROLE_ADMIN: [ROLE_VERIFIED_USER]
ROLE_SUPERADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_ALLOWED_TO_SWITCH: ~
My UserInterface->getRoles() method:
public function getRoles()
{
$roles = [$this->isVerified() ? 'ROLE_VERIFIED_USER' : 'ROLE_USER'];
/**
* #var UserSecurityRole $userSecurityRole
*/
foreach ($this->getUserSecurityRoles() as $userSecurityRole) {
$roles[] = $userSecurityRole->getRole();
}
return $roles;
}
ROLE_ADMIN must be explicitly assigned, yet isGranted('ROLE_ADMIN', $user) returns TRUE even if the user was just created and has not been assigned any roles other than the default ROLE_USER, as long as the currently logged in user is granted ROLE_ADMIN. This leads me to believe the 2nd argument to isGranted() is just ignored and that the Token provided to AccessDecisionManager->decide() by the SecurityContext is used instead.
If this is a bug I'll submit a report, but maybe I'm still doing something wrong?
You need only AccessDecisionManager for this, no need for security context since you don't need authentication.
$user = new Core\Model\User();
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
$isGranted = $this->get('security.access.decision_manager')
->decide($token, array('ROLE_ADMIN'));
This will correctly take role hierarchy into account, since RoleHierarchyVoter is registered by default
Update
As noted by #redalaanait, security.access.decision_manager is a private service, so accessing it directly is not a good thing to do.
It's better to use service aliasing, which allows you to access private services.
security.context Is deprecated since 2.6.
Use AuthorizationChecker:
$token = new UsernamePasswordToken(
$user,
null,
'secured_area',
$user->getRoles()
);
$tokenStorage = $this->container->get('security.token_storage');
$tokenStorage->setToken($token);
$authorizationChecker = new AuthorizationChecker(
$tokenStorage,
$this->container->get('security.authentication.manager'),
$this->container->get('security.access.decision_manager')
);
if (!$authorizationChecker->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
Maybe you can instantiate a new securityContext instance and use it to check if user is granted :
$securityContext = new \Symfony\Component\Security\Core\SecurityContext($this->get('security.authentication.manager'), $this->get('security.access.decision_manager'));
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, $this->container->getParameter('fos_user.firewall_name'), $user->getRoles());
$securityContext->setToken($token);
if ($securityContext->isGranted('ROLE_ADMIN')) {
// some stuff to do
}
I know this post is quite old, but I faced that problem recently and I created a service based on #dr.scre answer.
Here's how I did in Symfony 5.
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class AccessDecisionMaker
{
private AccessDecisionManagerInterface $accessDecisionManager;
public function __construct(AccessDecisionManagerInterface $accessDecisionManager)
{
$this->accessDecisionManager = $accessDecisionManager;
}
public function isGranted(UserInterface $user, string $role): bool
{
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
return $this->accessDecisionManager->decide($token, [$role]);
}
}
Now I can use it wherever I want.
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use Symfony\Component\Security\Core\Security;
class myClass
{
private Security $security;
private AccessDecisionMaker $decisionMaker;
public function __construct(Security $security, AccessDecisionMaker $decisionMaker)
{
$this->security = $security;
$this->decisionMaker = $decisionMaker;
}
public function someMethod(?User $user): void
{
$user = $user ?: $this->security->getUser();
if ($this->decisionMaker->isGranted($user, 'ROLE_SOME_ROLE')) {
// do something
} else {
// do something else
}
}
}
RoleVoter disregards the $object passed through from SecurityContext->isGranted(). This results in the RoleHierarchyVoter extracting roles from the Token instead of a provided UserInterface $object (if exists), so I had to find a different route.
Maybe there is a better way to go about this and if there is I'd sure like to know, but this is the solution I came up with:
First I implemented ContainerAwareInterface in my User class so I could access the security component from within it:
final class User implements AdvancedUserInterface, ContainerAwareInterface
{
// ...
/**
* #var ContainerInterface
*/
private $container;
// ...
public function setContainer(ContainerInterface $container = null)
{
if (null === $container) {
throw new \Exception('First argument to User->setContainer() must be an instance of ContainerInterface');
}
$this->container = $container;
}
// ...
}
Then I defined a hasRole() method:
/**
* #param string|\Symfony\Component\Security\Core\Role\RoleInterface $roleToCheck
* #return bool
* #throws \InvalidArgumentException
*/
public function hasRole($roleToCheck)
{
if (!is_string($roleToCheck)) {
if (!($roleToCheck instanceof \Symfony\Component\Security\Core\Role\RoleInterface)) {
throw new \InvalidArgumentException('First argument expects a string or instance of RoleInterface');
}
$roleToCheck = $roleToCheck->getRole();
}
/**
* #var \Symfony\Component\Security\Core\SecurityContext $thisSecurityContext
*/
$thisSecurityContext = $this->container->get('security.context');
$clientUser = $thisSecurityContext->getToken()->getUser();
// determine if we're checking a role on the currently authenticated client user
if ($this->equals($clientUser)) {
// we are, so use the AccessDecisionManager and voter system instead
return $thisSecurityContext->isGranted($roleToCheck);
}
/**
* #var \Symfony\Component\Security\Core\Role\RoleHierarchy $thisRoleHierarchy
*/
$thisRoleHierarchy = $this->container->get('security.role_hierarchy');
$grantedRoles = $thisRoleHierarchy->getReachableRoles($this->getRoles());
foreach ($grantedRoles as $grantedRole) {
if ($roleToCheck === $grantedRole->getRole()) {
return TRUE;
}
}
return FALSE;
}
From a controller:
$user = new User();
$user->setContainer($this->container);
var_dump($user->hasRole('ROLE_ADMIN'));
var_dump($this->get('security.context')->isGranted('ROLE_ADMIN'));
var_dump($this->get('security.context')->isGranted('ROLE_ADMIN', $user));
$user->addUserSecurityRole('ROLE_ADMIN');
var_dump($user->hasRole('ROLE_ADMIN'));
Output:
boolean false
boolean true
boolean true
boolean true
Although it does not involve the AccessDecisionManager or registered voters (unless the instance being tested is the currently authenticated user), it is sufficient for my needs as I just need to ascertain whether or not a given user has a particular role.
This looks like an issue with the:
abstract class AbstractToken implements TokenInterface
Look at the constructor. Looks like roles are created on instantiation and not queried at run time.
public function __construct(array $roles = array())
{
$this->authenticated = false;
$this->attributes = array();
$this->roles = array();
foreach ($roles as $role) {
if (is_string($role)) {
$role = new Role($role);
} elseif (!$role instanceof RoleInterface) {
throw new \InvalidArgumentException(sprintf('$roles must be an array of strings, or RoleInterface instances, but got %s.', gettype($role)));
}
$this->roles[] = $role;
}
}
Hence, the roles cannot change after the token has been created. I think the option is to write your own voter. I'm still looking around.
Create a service AccessDecisionMaker (used Shady's solution)
<?php
namespace Bp\CommonBundle\Service;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\SecurityContext;
class AccessDecisionMaker
{
/** #var Container */
private $container;
/** #var SecurityContext */
private $securityContext;
function __construct($container)
{
$this->container = $container;
if (!$this->securityContext) {
// Ensure security context is created only once
$this->securityContext = new SecurityContext($this->container->get(
'security.authentication.manager'
), $this->container->get('security.access.decision_manager'));
}
}
public function isGranted($roleToCheck, UserInterface $user)
{
if (!is_string($roleToCheck)) {
if (!($roleToCheck instanceof RoleInterface)) {
throw new \InvalidArgumentException('First argument expects a string or instance of RoleInterface');
}
$roleToCheck = $roleToCheck->getRole();
}
$token = new UsernamePasswordToken($user, null, $this->container->getParameter(
'fos_user.firewall_name'
), $user->getRoles());
$this->securityContext->setToken($token);
if ($this->securityContext->isGranted($roleToCheck)) {
return true;
}
return false;
}
}
Configure this as a service
bp.access_decision_maker:
class: Bp\CommonBundle\Service\AccessDecisionMaker
arguments: [#service_container ]
Use it
$this->container->get('bp.access_decision_maker')->isGranted("ROLE_ADMIN",$user);
You can use the security.context service with the isGranted method.
You can pass a second argument which is your object (see here).
In a controller:
$this->get('security.context')->isGranted('ROLE_FOOBAR', $myUser)

Categories