Extend UserProvider for FOS UserBundle - symfony

I am building a site using Symfony2 and it will be a white-label type of site, where multiple domains map to the same server. So coolsite.customer1.com and aservice.customer2.com would map to the same site, but would need to appear different to the end user. I already solved for the domains, and loading the unique configurations as a service.
With the FOS UserBundle setup and running with a custom user (that has the domain_id stored in it), registration, login, etc works fine except that users from domain1 can login to domain2 also. This is expected in the FOS UserBundle. I need to make modifications to the bundle so that it only will authenticate users on the domain they are assigned to.
I have created a userProvider that extends the original userProvider in FOS and have overridden the loadUserByUsername method to also check the domain. See below:
use FOS\UserBundle\Security\UserProvider as FOSProvider;
use Symfony\Component\DependencyInjection\ContainerInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Me\CoreBundle\Models\Core;
class UserProvider extends FOSProvider {
/**
*
* #var ContainerInterface
*/
protected $container;
public function __construct(UserManagerInterface $userManager, ContainerInterface $container) {
parent::__construct($userManager);
$this->container = $container;
}
/**
* {#inheritDoc}
*/
public function loadUserByUsername($username)
{
$core = $this->container->get('me_core');
/* #var $core Core */
$user = $this->findUserBy(array(
'username'=>$username,
'domain_id'=>$core->getDomainMap()->getId(),
));
if (!$user) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
return $user;
}
public function findUserBy(array $criteria) {
return $this->userManager->findUserBy($criteria);
}
}
I have configured the service with the following.
services:
me.security.authentication.userprovider:
class: Me\UserBundle\Security\UserProvider
arguments:
- #fos_user.user_manager
- #service_container
My security.yml looks like this:
security:
providers:
me.security.authentication.userprovider:
id: fos_user.user_provider.username
encoders:
FOS\UserBundle\Model\UserInterface: sha512
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
logout: true
anonymous: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/_wdt, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/public, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
- { path: ^/, role: ROLE_USER }
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
What happens when I try to access the site is an exception. "ServiceNotFoundException: The service "security.authentication.manager" has a dependency on a non-existent service "security.user.provider.concrete.fos_userbundle"."
I based my modifications on This Cookbook Recipe
Any ideas? I am thoroughly stumped on this.

I was able to get it to work. Turns out I needed to make the "id" the same as the name of the service I was using. The commented lines are the originals that came with the bundle.
security:
providers:
me.security.authentication.userprovider:
id: me.security.authentication.userprovider
#fos_userbundle:
#id: fos_user.user_provider.username

Related

An error occurred while loading the web debug toolbar

I am working on Symfony 3.4 project, and have been facing a weird issue. The web debug toolbar fails to load and instead gives an error "An error occurred while loading the web debug toolbar. Open the web profiler." Here is the screenshot
And when I click on Open the web profiler link it takes me to another exception page. Here is its screenshot
So after hours of debugging I was able to figure out that the issue is inside a
custom listener. It is registered in my services.yml as follows:
services:
language.kernel_request_listener:
class: TraceBundle\Listeners\LanguageListener
arguments:
- "#service_container"
tags:
- { name: kernel.event_listener, event: kernel.request, method: setLocale }
And here is the LanguageListener.php:
<?php
namespace TraceBundle\Listeners;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class LanguageListener{
private $token_storage;
private $securityContext;
private $container;
public function __construct(ContainerInterface $containerInterface)
{
$this->container = $containerInterface;
$this->securityContext = $this->container->get('security.authorization_checker');
$this->token_storage = $this->container->get('security.token_storage');
}
public function setLocale(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
if ($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$user = $this->token_storage->getToken()->getUser();
$userLocale = $user->getTenant()->getLanguage()->getValue();
$tenantid = $this->container->get('tenant_manager')->getTenantId($user);
$request = $event->getRequest();
$request->attributes->set('tenantid', $tenantid);
if ($userLocale) {
$request->setLocale($userLocale);
$translator = $this->container->get('translator');
$translator->setLocale($userLocale);
}
}
}
}
Now when I comment the following lines:
if ($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$user = $this->token_storage->getToken()->getUser();
$userLocale = $user->getTenant()->getLanguage()->getValue();
$tenantid = $this->container->get('tenant_manager')->getTenantId($user);
$request = $event->getRequest();
$request->attributes->set('tenantid', $tenantid);
if ($userLocale) {
$request->setLocale($userLocale);
$translator = $this->container->get('translator');
$translator->setLocale($userLocale);
}
the error goes away and the profiler loads as expected.
I've tried var_dump() after every line and all the values seem to be fine. the service tenant_manager works fine as well as the translator service. What am I missing here? Let me know if you need more code.
Thanks in advance!
EDIT: As requested here is my security.yml:
security:
# https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
providers:
in_memory:
memory: ~
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
form_login:
success_handler: authentication.handler.login_success_handler
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
# logout: true
logout:
path: /logout
target: /login
anonymous: true
js_router:
pattern: ^/(js\/routing)
security: false
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/dashboard, role: ROLE_ADMIN }
- { path: ^/campaigns, role: ROLE_ADMIN }
- { path: ^/dashboard, role: ROLE_ADMIN }
- { path: ^/lives, role: ROLE_ADMIN }
- { path: ^/colleagues, role: ROLE_ADMIN }
- { path: ^/addcolleague, role: ROLE_ADMIN }
- { path: ^/adminpage, role: ROLE_ADMIN }
- { path: ^/test, role: ROLE_ADMIN }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
I had the exact same problem using a custom Listener with TokenStorageInterface.
Here's a scaled down version of my listener code:
class DatabaseUserAuthenticationListener {
private $authToken;
public function __construct(TokenStorageInterface $tokenStorage) {
if ($tokenStorage->getToken()) {
$this->authToken = $tokenStorage->getToken();
}
}
public function onKernelController(FilterControllerEvent $event) {
if ($this->authToken) {
$this->authToken->setAttribute("blah", true);
}
}
}
In my case the offending line was $this->authToken->setAttribute("blah", true);. $this->authToken ends up being null when _wdt routes are called (since they have no user context). At least that's my theory.
#Pavel is correct in that Symfony is setting the token to null between requests, although I don't think it's security: false that's doing it.
Checking that your token exists and is not null or empty before working with it (if ($this->authToken) {...}) fixes the issue (at least for me).
#utkarsh2k2, I'm sure you already fixed your issue... if not, you could try checking that $this->token_storage->getToken() has something in it before calling ->getUser().
I made some experiments with your code and found this:
Removing these lines solves the problem:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
also replacing them with these helps:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
anonymous: true
So I can conclude that security: false leads to setting security token to null behind the scenes.
So far I didn't locate this mechanism (will keep trying), so would be grateful for any help.
Another solution is to check if token is not null right inside of your listener:
if (null !== $this->token_storage->getToken()
&& $this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
...
}
But it makes your code to care about situation caused by your dev config (dev firewall) so I decide it's not the best way.
Any comments/additions are welcome.
The solution for me was, when I star a proyect almost clean still have that problem:
"An error occurred while loading the web debug toolbar. Open the web profiler."
My Solution:
I added a .htaccess to the public directory
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ ./index.php/$1 [QSA,L]
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
With that only the toolbar appear.

Symfony app login and Sonata admin dashboard login

In my app I have 2 parts, a frontend generated manually and a backend dashboard generated with Sonata admin. And I have an issue with the login of both parts:
app/config/security.yml
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
m_and_m\UsuariosBundle\Entity\Usuarios: { algorithm: sha512, iterations: 10 }
m_and_m\ClientesBundle\Entity\Clientes: { algorithm: sha512, iterations: 10 }
access_control:
# URL of FOSUserBundle which need to be available to anonymous users
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
# Admin login page needs to be access without credential
- { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
# Secured part of the site
# This config requires being logged for the whole site and having the admin role for the admin part.
# Change these rules to adapt them to your needs
- { path: ^/admin/login ,role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/logout ,role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/* ,role: [ROLE_ADMIN, ROLE_SONATA_ADMIN,TOTAL] }
- { path: ^/usuarios/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/usuarios/registro, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/usuarios/*, roles: ROLE_USUARIO }
- { path: ^/clientes/*, roles: ROLE_CLIENTES }
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN,ROLE_USUARIO,ROLE_CLIENTE]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
#SONATA:
#- ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT # If you are using acl then this line must be commented
providers:
fos_userbundle:
id: fos_user.user_manager
chain_provider:
chain:
providers: [usuarios, clientes]
usuarios:
entity: { class: m_and_m\UsuariosBundle\Entity\Usuarios, property: email }
clientes:
entity: { class: m_and_m\ClientesBundle\Entity\Clientes, property: email }
firewalls:
# -> custom firewall for the admin area of the URL
admin:
pattern: /admin(.*)
context: user
form_login:
provider: fos_userbundle
login_path: /admin/login
use_forward: false
check_path: /admin/login_check
failure_path: /admin/login
logout:
path: /admin/logout
target: /admin
anonymous: true
# -> end custom configuration
# default login area for standard users
frontend:
pattern: ^/*
context: frontend
provider: chain_provider
anonymous: ~
form_login:
login_path: usuarios_login
check_path: usuarios_login_check
logout:
path: usuarios_logout
UsuariosBundle/Resources/config/services.yml
services:
usuarios.admin.usuarios:
class: m_and_m\UsuariosBundle\Admin\UsuariosAdmin
arguments: [~, m_and_m\UsuariosBundle\Entity\Usuarios, SonataAdminBundle:CRUD]
tags:
- {name: sonata.admin, manager_type: orm, group: Usuarios(Admin), label: Usuarios}
login_listener:
class: m_and_m\UsuariosBundle\Listener\LoginListener
arguments: [#security.context, #router]
tags:
- { name: kernel.event_listener, event: security.interactive_login }
- { name: kernel.event_listener, event: kernel.response }
Following the doc I have created a LoginListener in my usuariosbundle.
When I log in from the frontend all goes perfect. But the Sonata admin dashboard login goes to the LoginListener.php file and returns an error:
FatalErrorException: Error: Call to undefined method Application\Sonata\UserBundle\Entity\User::getNombre() in
C:\wamp\www\m_and_m\src\m_and_m\UsuariosBundle\Listener\LoginListener.php
line 24
LoginListener.php
<?php
namespace m_and_m\UsuariosBundle\Listener;
use Symfony\Componene\EventDispatcher\Event;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
class LoginListener
{
private $contexto, $router, $usuario=null,$role=null;
public function __construct(SecurityContext $context, Router $router)
{
$this->contexto=$context;
$this->router = $router;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$token = $event->getAuthenticationToken();
$this->usuario = $token->getUser()->getNombre();
$this->role = $token->getRoles();
$this->role=$this->role[0]->getrole();
if($this->role=='ROLE_CLIENTE' && $token->getUser()->getActivo()==false){
$this->usuario = null;
}
}
public function onKernelResponse(FilterResponseEvent $event)
{
if(null != $this->usuario)
{
if($this->role=='ROLE_USUARIO'){
$portada=$this->router->generate('portada_usuario');
}
else{
$portada=$this->router->generate('portada_cliente');
}
$event->setResponse(new RedirectResponse($portada));
$event->stopPropagation();
}
}
}
?>
And I donĀ“t know why is this happening, cause both providers are in different context. Do I need to do something more to separate both logins?
You are calling a method that does not exist in that entity. If you pay attention in the error you will see that the user class that comes from the token is not the one you think it is.

FOS UserBundle Access Denied

I'm trying to use FOS UserBundle to manage users on my project.
Here is my security.yml file :
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
FOS\UserBundle\Model\UserInterface: sha512
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
in_memory:
memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
logout: true
anonymous: true
access_control:
#- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
I have manually set my user's rights in my controller like below :
public function testGroupsAction( UserInterface $user )
{
$user->addRole("ROLE_ADMIN");
$this->getDoctrine()->getManager()->persist($user);
$this->getDoctrine()->getManager()->flush();
echo "<pre>";
\Doctrine\Common\Util\Debug::dump($user->getRoles());
echo "</pre>";die;
}
the $user->getRoles() function returns me an array with all my user's roles :
array (size=3)
0 => string 'ROLE_SUPER_ADMIN' (length=16)
1 => string 'ROLE_ADMIN' (length=10)
2 => string 'ROLE_USER' (length=9)
(ROLE_SUPER_ADMIN has been added during my tests)
However when i try to reach a route like "/admin/my/route"n i've got a 403 access forbidden.
Any idea why Symfony doesn't want my user to access admin pages?
Edit :
When i look in the profiler, the user only has [ROLE_USER]...
Thank you.
I finally got it working.
Thanks to Zizoujab, I tried FOSUserBundle's commands to promote a user :
> php app/console fos:user:promote myUser
It worked perfectly well. However, as I have no ssh access nor any other command line tool on my server, i needed to do it via PHP code.
So i went to the Command code FOS\UserBundle\Command\PromoteUserCommand which uses the FOS\UserBundle\Util\UserManipulator to do actions on the user.
So if you want to modify your User directly in your controller, you can use it, but I don't know if it is the best way to do it. Just call it via your container like this :
/**
* #Route("/user/{id}", name="test_user")
* #ParamConverter("user" , class="MyBundle:User")
*/
public function testUserAction( UserInterface $user )
{
$userManipulator = $this->container->get("fos_user.util.user_manipulator");
$userManipulator->addRole($user,'ROLE_ADMIN');
return new Response();
}
Hope it helps.

Symfony2 FOS, redirect when not logged in

I'd like to redirect user to login page when nobody is not logged in. I wrote lister to that, but all the time I'm getting ERR_TOO_MANY_REDIRECTS error.
Maybe is another method to achieve it globaly, and redirect user. Checking if user is logged in every controller is not a solution.
Listener\AccessListener.php
namespace Main\UserBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\SecurityContext;
class AccessListener{
private $security;
private $router;
public function __construct($security, $router){
$this->security = $security;
$this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event){
if ($event->isMasterRequest()) {
if( $this->security->isGranted('IS_AUTHENTICATED_REMEMBERED') ){
$url = $this->router->generate('fos_user_security_login');
$event->setResponse(new RedirectResponse($url));
}
}
}
}
security.yml
security:
access_denied_url: /login
encoders:
FOS\UserBundle\Model\UserInterface: sha512
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
always_use_default_target_path: false
default_target_path: /
logout: true
anonymous: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
How do I correct it?
Your listener is checking whether a user is logged in and then, if they are not, they are being forwarded to the login page. At which point the listener checks whether the user is logged in... and so on.. and so on.
To stop this redirect loop you could check whether the current route being requested is the route that you are forwarding to, like so...
public function onKernelRequest(GetResponseEvent $event){
if ($event->isMasterRequest()) {
$loginRoute = 'fos_user_security_login';
$request = $event->getRequest();
// Return if current route and login route match
if ($request->get('_route') === $loginRoute) {
return;
}
if( $this->security->isGranted('IS_AUTHENTICATED_REMEMBERED') ){
$url = $this->router->generate($loginRoute);
$event->setResponse(new RedirectResponse($url));
}
}
}
However, a better way of doing this would be to add the root to the access control section needing a logged in user. This would allow you to have a better control over accessible and inaccessible paths.
access_control:
... current stuff ...
- { path: ^/, role: ROLE_USER }
You need to check whether the current security context holds (or in your case if it does not hold) a fully authenticated user session as follows:
if( false == $this->security->isGranted('IS_FULLY_AUTHENTICATED') ){

How to set a login form for admins and another for other users using FOSUserBundle?

When having a backend for admin users, it is interesting to have a login form, and at the same time having a normal login form for normal users in the public area of our website.
Is that possible using FOSUserBundle? How can it be done "the Symfony2" way?
First we need to configure some special routes for the admin area:
admin_login:
pattern: /admin/login
defaults: { _controller: FOSUserBundle:Security:login }
admin_login_check:
pattern: /admin/login_check
defaults: { _controller: FOSUserBundle:Security:check }
admin_logout:
pattern: /admin/logout
defaults: { _controller: FOSUserBundle:Security:logout }
Next configure a special firewall for the admin area using these routes, and define them to be anonymously accessed:
firewalls:
...
admin:
pattern: /admin/(.*)
form_login:
provider: fos_userbundle
login_path: admin_login
check_path: admin_login_check
default_target_path: yourproject_admin_default_index
logout:
path: admin_logout
target: admin_login
anonymous: true
context: application
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
context: application
...
access_control:
...
- { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
Ok! We have just separated our login system in two parts: admin and main.
Let's override the SecurityController. For that we will need to create a custom bundle which parent is FOSUserBundle (check the doc for that). In this new bundle, create the controller:
<?php
namespace YourProject\UserBundle\Controller;
use FOS\UserBundle\Controller\SecurityController as BaseController;
/**
* {#inheritDoc}
*/
class SecurityController extends BaseController
{
/**
* {#inheritDoc}
*/
public function renderLogin(array $data)
{
$requestAttributes = $this->container->get('request')->attributes;
if ('admin_login' === $requestAttributes->get('_route')) {
$template = sprintf('AdminBundle:Security:login.html.twig');
} else {
$template = sprintf('FOSUserBundle:Security:login.html.twig');
}
return $this->container->get('templating')->renderResponse($template, $data);
}
}
That's it! Now you can write your AdminBundle:Security:login.html.twig :)
NOTE: Don't forget to use the admin routes in your admin area! (in the login form action, the logout link, etc)
Regarding to the approved answer, I made some adjustments in my Symfony 3.2.8 project in order to work correctly.
Instead of
$requestAttributes = $this->container->get('request')->attributes;
in the Security Controller, I used $requestAttributes = $this->container->get('request_stack')->getCurrentRequest();.

Resources