Is there any event when Symfony authenticates a users from the session? LoginSuccessEvent seems only to be fired during an interactive login, e.g. when using a login form but not when a new page is loaded for an already logged in user.
Details:
I am working on a Symfony 6.1 based project and I followed the instructions in the Symfony docs (e.g. here and here) to build security based on a user class and a login form.
Everything works fine: Access to restricted pages is blocked by the firewall, the user is redirected to the login form where he can login. Once logged in, all pages can be accessed until the login expires.
After submitting the login form LoginSuccessEvent is fired. However, when loading a new page using the user from the session, this event is not fired. Is there any other security event for this kind of authentication? None of the events I found in the docs seem to apply to this case.
Indeed, the event is executed only once at login. Alternatively, you can create a kernel.request event that will be fired on every request. In the event, you can pass the Security component to the constructor arguments and get the current user and perform the necessary actions
use App\Entity\User;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Security;
class UserListener
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function onKernelRequest(RequestEvent $event)
{
$user = $this->security->getUser();
if (!$event->getRequest()->isXmlHttpRequest() and $user instanceof User){
// do something ...
dump($user);
}
}
}
Registering a listener in services.yaml
App\EventListener\UserListener:
tags:
- {name: kernel.event_listener, event: kernel.request }
Situation
I have a fairly standard User model with the Billable trait. I'm using Stripe in this project, and User represents my stripe customers.
Per laravel docs, I've got the following code in the User model:
protected static function booted() {
static::updated(queueable(function ($user) { $user->syncStripeCustomerDetails(); }));
}
This is my logout code in LoginController#logout:
public function logout() {
auth()->logout();
return redirect("/login");
}
What's happening
Whenever I navigate to the /logout route, I get this error:
Stripe\Exception\InvalidArgumentException.
The resource ID cannot be null or whitespace.
I'm assuming there's some conflict between the code that expects to sync the User object with Stripe...
If I refresh the page, all seems to work fine.
Expected behaviour
User syncs to Stripe customer. System logs user out and navigates to /login without any error.
Any pointers? Thanks in advance.
I've encountered a new issue on my Symfony application and I cannot find anything to help me.
In my database, a user has an activation_token field.
When the user creates his account on my website, it generates a random token that will be used to validate his account. It will send him an email with the token in order to verify/activate his account and when he will click on it, it will set the activation_token field to null.
Creating the account works. Sending the email works. Validating the account works. I only have 1 issue.
I'd like to add a constraint that when the activation_token field is not null, the user won't be able to log in. In short, the user will need to validate his account at first in order to be able to log in. But I have no clue how to add a constraint like that.
Currently, I can log in even if I didn't validate my account and that's exactly what I want to prevent.
I haven't created a custom login form in PHP, I'm using the Security bundle from Symfony. Here we have a screenshot of my security.yaml file.
I don't know if it's possible to add a constraint with the Security bundle or not.
Expanding on Cerad's comment to the question:
If you are using Symfony >= 4.4 you can write a custom UserChecker. From the documentation:
<?php
namespace App\Security;
use App\Entity\User as AppUser;
use App\Exception\AccountDeletedException;
use Symfony\Component\Security\Core\Exception\AccountExpiredException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
// this is your custom check:
if (null !== $user->getActivationToken()) {
throw new CustomUserMessageAccountStatusException('Please validate your account first, before logging in.');
}
}
public function checkPostAuth(UserInterface $user)
{
// nothing to check here.
return;
}
}
Add the user checker to your security configuration like this:
# config/packages/security.yaml
# ...
security:
firewalls:
main:
pattern: ^/
user_checker: App\Security\UserChecker
For version before Symfony 4.1
Instead of having your User Entity implementing the basic UserInterface, you can implement AdvancedUserInterface which contains more functions usefull for activating/deactivating user, locking user, expiring user etc
Here is the link of the documentation page: https://symfony.com/doc/4.0/security/entity_provider.html#forbid-inactive-users-advanceduserinterface
In your case, you may need to add this kind of logic:
public function isEnabled()
{
return null === $this->validationToken;
}
I make use of SonataAdminBundle in Symfony 3. Because I use Symfony 3, I still can't make use of SonataUserBundle. So I am using SonataAdminBundle with FOSUserBundle only.
Now what I try to achieve is to hide specific routes per role. For example, I only have three roles;
Super Admin
Admin
Another role
Super Admin has all the roles admin has, admin has all of the third one, and the third one has ROLE_USER obviously. Super Admin should be able to create new users and assign a role to him. The Super Admin should also be able to change user's passwords. The users should be able to change the passwords of their own accounts. And finally, other roles that Super Admin should not be able to change their own roles and to create new users.
How can I achieve this without using SonataUserBundle. For the removing of routes part I tried something like this:
protected function configureRoutes(RouteCollection $collection)
{
$securityContext = $this->getConfigurationPool()->getContainer()->get('security.authorization_checker');
if (!$securityContext->isGranted('ROLE_SUPER_ADMIN')) {
$collection->remove('create');
$collection->remove('edit');
}
}
But I guess there is a better solution. I am completely aware of the official documentation about security but I'm confused with that, does that mean I have to hard code each and every single role for all different Admins in my security.yml file? Does this even work without SonataUserBundle? I don't want to add extra database tables for ACL.
Can somebody please assist and/or provide a good example? I'll really appreciate it a lot.
How to manage users and roles in Sonata without SonataUserBundle?
Answer: we need to do the same as SonataUserBundle. (But let's simplify a little)
An analogy about security based on ROLE_ in Symfony flat:
The house: A building that has doors and keys (the system).
The door: Place in the house where access is restricted - isGranted():
// the door is here, we need the key to open it.
if ($this->isGranted('ROLE_FOO')) {
// restricted access to do something
}
The key: Granted permission to access a restricted door - ROLE_*:
class User extends FOSUser
{
public function getRoles()
{
// the keys comes from DB or manually.
// e.g:
return ['ROLE_FOO'];
}
}
The master key: A key that can open several doors:
# app/config/security.yml
security:
role_hierarchy:
# other than opening the door "isGranted('ROLE_BAR')"
# we can also opening the door "isGranted('ROLE_FOO')" with this single key.
ROLE_BAR: ROLE_FOO
Following this analogy, SonataAdminBundle already has created the doors to restrict access to each default action (e.g. list action) across an entity managed.
So our job is to assign the keys to users "only" (unless you need to create your own doors). There are many ways to achieve this (it'll depend on what you need).
Note: If you don't have a role hierarchy, you have single keys only (i.e. you don't have master keys), which makes it less flexible assignment of roles (keys).
Now, SonataAdminBundle uses a particular way to check the keys in a context of admin class, just doing the following: $admin->isGranted('list'), this is because he has his own isGranted() function (where 'list' is the action name), but really what it does is build the role name (by using the current admin code) before check it, so he verify this finally: isGranted('ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_LIST') -this key it's what we need "give" to the user-.
How to get the role list from Sonata admin system?
In a controller context:
public function getSonataRoles()
{
$roles = [];
// the sonata admin container
$pool = $this->get('sonata.admin.pool');
foreach ($pool->getAdminServiceIds() as $id) {
// gets the registered admin instance from id service name
$admin = $pool->getInstance($id);
// the role security handler instance (must be configured)
$securityHandler = $admin->getSecurityHandler();
// gets the base role name from admin code
// e.g. 'ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_%s'
$baseRole = $securityHandler->getBaseRole($admin);
// gets the access actions (e.g. LIST, CREATE, EDIT, etc.)
foreach (array_keys($admin->getSecurityInformation()) as $action) {
// add the final role name
// e.g. 'ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_LIST'
$roles[] = sprintf($baseRole, $action);
}
}
return $roles;
}
Next, you can do anything with that (e.g. create a custom form type to manage the user roles property). You could to sort, grouping these roles to show the user this list in the simplest possible way.
Up here, we can assign roles and work without using even the role_hierarchy.
More details http://symfony.com/doc/current/bundles/SonataAdminBundle/reference/security.html
You can define a custom user permission Voter for your User entity, see here.
namespace AppBundle\Security;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class UserVoter extends Voter
{
private $decisionManager;
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject)
{
// only vote on User objects inside this voter
if (!$subject instanceof User) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
// ROLE_SUPER_ADMIN can do anything! The power!
if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
return true;
}
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
/** #var User $targetUser */
$targetUser = $subject;
// Put your custom logic here
switch ($attribute) {
case "ROLE_SONATA_ADMIN_USER_VIEW":
return true;
case "ROLE_SONATA_ADMIN_USER_EDIT":
return ($user === $targetUser);
}
return false;
}
}
Then you create the service
sonata_admin.user_voter:
class: AppBundle\Security\UserVoter
arguments: ['#security.access.decision_manager']
public: false
tags:
- { name: security.voter }
Be carefull of the access decision strategy, I may not work depending on your configuration if it's defined to unanimous or consensus
You may also add a direct link/route to the user's own edit page if you don't want to give every user access to the user list.
EDIT
To restrict user role edition, as you don't want a user to edit its own role, you can simply edit the configureFormFields function :
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('username')
->add('plainPassword', 'text', array(
'required' => false,
)
)
/* your other fields */
;
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$formMapper->add('roles', \Symfony\Component\Form\Extension\Core\Type\CollectionType::class, array(
'entry_type' => \Symfony\Component\Form\Extension\Core\Type\ChoiceType::class,
'entry_options' => array(
'choices' => array(
"ROLE_OPTICKSB2B" => "ROLE_OPTICKSB2B",
"ROLE_ADMIN" => "ROLE_ADMIN",
"ROLE_SUPER_ADMIN" => "ROLE_SUPER_ADMIN"
),
)
));
}
$formMapper
->add('isActive')
->add('title')
->add('firstname')
->add('lastname')
;
}
Obviously, Symfony forms component will check for you than no other field are added.
We're building a business app from the ground up in Symfony 2, and I've run into a bit of a snag with the user registration flow: after the user creates an account, they should be automatically logged in with those credentials, instead of being immediately forced to provide their credentials again.
Anyone had any experience with this, or able to point me in the right direction?
Symfony 4.0
This process hasn't changed from Symfony 3 to 4 but here is an example using the newly recommended AbstractController. Both the security.token_storage and the session services are registered in the parent getSubscribedServices method so you don't have to add those in your controller.
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use YourNameSpace\UserBundle\Entity\User;
class LoginController extends AbstractController{
public function registerAction()
{
$user = //Handle getting or creating the user entity likely with a posted form
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->container->get('security.token_storage')->setToken($token);
$this->container->get('session')->set('_security_main', serialize($token));
// The user is now logged in, you can redirect or do whatever.
}
}
Symfony 2.6.x - Symfony 3.0.x
As of Symfony 2.6 security.context is deprecated in favor of security.token_storage. The controller can now simply be:
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use YourNameSpace\UserBundle\Entity\User;
class LoginController extends Controller{
public function registerAction()
{
$user = //Handle getting or creating the user entity likely with a posted form
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->get('security.token_storage')->setToken($token);
$this->get('session')->set('_security_main', serialize($token));
}
}
While this is deprecated you can still use security.context as it has been made to be backward compatible. Just be ready to update it for Symfony 3.
You can read more about the 2.6 changes for security here: https://github.com/symfony/symfony/blob/2.6/UPGRADE-2.6.md
Symfony 2.3.x
To accomplish this in Symfony 2.3 you can no longer just set the token in the security context. You also need to save the token to the session.
Assuming a security file with a firewall like:
// app/config/security.yml
security:
firewalls:
main:
//firewall settings here
And a controller action similar to:
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use YourNameSpace\UserBundle\Entity\User;
class LoginController extends Controller{
public function registerAction()
{
$user = //Handle getting or creating the user entity likely with a posted form
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->get('security.context')->setToken($token);
$this->get('session')->set('_security_main',serialize($token));
//Now you can redirect where ever you need and the user will be logged in
}
}
For the token creation you will want to create a UsernamePasswordToken. This accepts 4 parameters: User Entity, User Credentials, Firewall Name, User Roles. You don't need to provide the user credentials for the token to be valid.
I'm not 100% sure that setting the token on the security.context is necessary if you are just going to redirect right away. But it doesn't seem to hurt so I have left it.
Then the important part, setting the session variable. The variables naming convention is _security_ followed by your firewall name, in this case main making _security_main.
Figured this one out, finally.
After user registration, you should have access to an object instanceof whatever you've set as your user entity in your provider configuration. The solution is to create a new token with that user entity and pass it into the security context. Here's an example based on my setup:
RegistrationController.php:
$token = new UsernamePasswordToken($userEntity, null, 'main', array('ROLE_USER'));
$this->get('security.context')->setToken($token);
Where main is the name of the firewall for your application (thanks, #Joe). That's really all there is to it; the system now considers your user fully logged in as the user they've just created.
EDIT: Per #Miquel's comment, I've updated the controller code sample to include a sensible default role for a new user (though obviously this can be adjusted according to your application's specific needs).
If you have a UserInterface object (and that should be the case most of the time) you might want to use the getRoles function that it implements for the last argument.
So if you create a function logUser, it should looks like that:
public function logUser(UserInterface $user) {
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->container->get('security.context')->setToken($token);
}
I'm using Symfony 2.2 and my experience was slightly different than Problematic's, so this is a combined version of all the info from this question plus some of my own.
I think Joe is wrong about the value of $providerKey, the third parameter to the UsernamePasswordToken constructor. It's supposed to be the key of an authentication (not user) provider. It's used by the authentication system to distinguish between tokens created for different providers. Any provider which descends from UserAuthenticationProvider will only authenticate tokens whose provider key matches its own. For example, the UsernamePasswordFormAuthenticationListener sets the key of the token it creates to match that of its corresponding DaoAuthenticationProvider. That lets a single firewall have multiple username+password providers without them stepping on each other. We therefore need to choose a key that won't conflict with any other providers. I use 'new_user'.
I have a few systems in other parts of my application that depend on the authentication success event, and that isn't fired by just setting the token on the context. I had to get the EventDispatcher from the container and fire the event manually. I decided against also firing an interactive login event because we're authenticating the user implicitly, not in response to an explicit login request.
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
$user = // get a Symfony user instance somehow
$token = new UsernamePasswordToken(
$user, null, 'new_user', $user->getRoles() );
$this->get( 'security.context' )->setToken( $token );
$this->get( 'event_dispatcher' )->dispatch(
AuthenticationEvents::AUTHENTICATION_SUCCESS,
new AuthenticationEvent( $token ) );
Note that use of $this->get( .. ) assumes the snippet is in a controller method. If you're using the code somewhere else you'll have to change those to call ContainerInterface::get( ... ) in a way appropriate to the environment. As it happens my user entities implement UserInterface so I can use them directly with the token. If yours don't you'll have to find a way to convert them to UserInterface instances.
That code works, but I feel like it's hacking around Symfony's authentication architecture rather than working with it. It would probably be more correct to implement a new authentication provider with its own token class rather than hijacking the UsernamePasswordToken. Also, using a proper provider would mean that the events were handled for you.
With Symfony 4.4, you can simply do the following in your controller method (see from the Symfony documentation: https://symfony.com/doc/current/security/guard_authentication.html#manually-authenticating-a-user):
// src/Controller/RegistrationController.php
// ...
use App\Security\LoginFormAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
class RegistrationController extends AbstractController
{
public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request)
{
// ...
// after validating the user and saving them to the database
// authenticate the user and use onAuthenticationSuccess on the authenticator
return $guardHandler->authenticateUserAndHandleSuccess(
$user, // the User object you just created
$request,
$authenticator, // authenticator whose onAuthenticationSuccess you want to use
'main' // the name of your firewall in security.yaml
);
}
}
One important thing, make sure your firewall is not set to lazy. If it is, the token will never be stored in the session and you will never get logged in.
firewalls:
main:
anonymous: ~ # this and not 'lazy'
In case anyone has the same follow-on question which kept me coming back to here:
Calling
$this->container->get('security.context')->setToken($token);
only effects the current security.context for the route used.
I.e. you can only log in a user from a url within the firewall's control.
(Add an exception for the route if needed - IS_AUTHENTICATED_ANONYMOUSLY)
As Problematic here already mentioned, this elusive $providerKey parameter is in reality nothing more than the name of your firewall rule, 'foobar' in the case of the example below.
firewalls:
foobar:
pattern: /foo/
I tried all the answers here and none worked. The only way I could authenticate my users on a controller is by making a subrequest and then redirecting. Here is my code, I'm using silex but you can easily adapt it to symfony2:
$subRequest = Request::create($app['url_generator']->generate('login_check'), 'POST', array('_username' => $email, '_password' => $password, $request->cookies->all(), array(), $request->server->all());
$response = $app->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, false);
return $app->redirect($app['url_generator']->generate('curriculos.editar'));
On Symfony version 2.8.11 (probably working for older and newer versions), if you use FOSUserBundle simply do this :
try {
$this->container->get('fos_user.security.login_manager')->loginUser(
$this->container->getParameter('fos_user.firewall_name'), $user, null);
} catch (AccountStatusException $ex) {
// We simply do not authenticate users which do not pass the user
// checker (not enabled, expired, etc.).
}
No need to dispatch event as I've seen in other solutions.
inpired from FOS\UserBundle\Controller\RegistrationController::authenticateUser
(from composer.json FOSUserBundle version : "friendsofsymfony/user-bundle": "~1.3")