Giter Site home page Giter Site logo

Comments (16)

scheb avatar scheb commented on July 26, 2024

If the 2fa is not shown, what's shown instead? I've seen that your entire application is protected with - { path: ^/, role: ROLE_USER }, so none of the pages should be accessible after login.

What happens when you navigate to /2fa manually? Does it show the 2fa form?

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

All the pages are accessible(based on dynamic roles I have) except /2fa, that redirects me to access denied page I have.

My User entity role getter updated with custom roles I have in my permissions table, which gets assigned based on user group to different users. Because in user table by default all user will have ROLE_USER while admin will have ROLE_ADMIN, others will be from permissions and group relationship as shown in User entity getRoles().

public function getRoles()
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        // Add group assinged roles
        if ($this->typegroup) {
            $permissions = $this->typegroup->getPermissions();
            if ($permissions) {
                foreach ($permissions as $permission) {
                    $roles[] = $permission->getRole();
                }
            }
        }

        return array_unique($roles);
    }

I also have event listener which gets called on SecurityEvents::INTERACTIVE_LOGIN

My custom event subscriber class which adds all the permissions(ROLES) to admin/superadmin users.

<?php

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use App\Entity\Permission;
use App\Entity\FrontendConfig;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

class ControllerSubscriber implements EventSubscriberInterface
{
    private $tokenStorage;

    private $em;

    protected $userManager;

    protected $authenticationContextProvider;

    public function __construct(TokenStorageInterface $tokenStorage, 
                                EntityManagerInterface $em, 
                                UserManagerInterface $userManager, 
                                SessionInterface $session)
    {
        $this->tokenStorage = $tokenStorage;
        $this->em = $em;
        $this->userManager = $userManager;
        $this->session = $session;
    }

    public function onKernelController(ControllerEvent $event)
    {
        $frontEndConfig = $this->em->getRepository(FrontendConfig::class)->findAll();
        $config_arr = [];
        foreach($frontEndConfig as $config) {
            $config_arr[$config->getAliasName()] = $config->getValue();
        }

        $this->session->set('frontEndConfig', $config_arr);
    }

    /**
     * @param InteractiveLoginEvent $event
     */
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $token = $this->tokenStorage->getToken();
        $user = $token->getUser();

        if ($user instanceof UserInterface) {
            //Add all permissions for admin and superadmin users
            $user_roles = $user->getRoles();

            if (in_array('ROLE_ADMIN', $user_roles) || in_array('ROLE_SUPER_ADMIN', $user_roles) || in_array('ROLE_TEACHER', $user_roles)) {

                if (in_array('ROLE_TEACHER', $user_roles)) {
                    $permissionsNeedToBeMerged = [  'ROLE_CREATE_MEETING', 
                                                    'ROLE_VIEW_AGENDA_MEETINGS', 
                                                    'ROLE_VIEW_AGENDA_HOLIDAYS',
                                                    'ROLE_VIEW_AGENDA_BREAKS'
                                                ];
                    $user_roles = array_merge($user_roles, $permissionsNeedToBeMerged);
                } else {
                    $permissions = $this->em->getRepository(Permission::class)->findAll();
                    foreach ($permissions as $permission) {
                        $user_roles[] = $permission->getRole();
                    }
                }

                // there didn't seem to be an easier way to grab the provider key, 
                // so using bound closure to retrieve it
                $providerKeyGetter = function($token) {
                    return $token->providerKey;
                };
                $boundProviderKeyGetter = \Closure::bind($providerKeyGetter, null, $token);

                // check & load roles for user here if necessary

                $this->tokenStorage->setToken(
                    new UsernamePasswordToken(
                        $user,
                        $token->getCredentials(),
                        $boundProviderKeyGetter($token), //main == firewall setting
                        array_unique($user_roles)
                    )
                );
            }
         }
    }

    public static function getSubscribedEvents()
    {
        return [
            ControllerEvent::class => 'onKernelController',
            SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
        ];
    }
}

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

ControllerSubscriber::onSecurityInteractiveLogin looks like a problem to me, It replaces the security token (which should be a TwoFactorToken at that point) with a UsernamePasswordToken. This will skip the 2fa process, which requires the TwoFactorToken to be present.

I'd suggest you change your code in ControllerSubscriber from:

if ($user instanceof UserInterface)

to:

if ($user instanceof UserInterface && $token instanceof UsernamePasswordToken)

This will make sure:

  • the TwoFactorToken is kept on login
  • your code is still executed, once 2fa was completed

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

Alternatively, instead of using SecurityEvents::INTERACTIVE_LOGIN, you could also subscribe to the AuthenticationEvents::AUTHENTICATION_SUCCESS event, which is only dispatched after 2fa was completed.

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

Wow, super fast. Thanks for the replies. It now redirects to 2fa but could not load form, giving below error.

Unable to find template "security/2fa_form.html.twig"

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

Well, that is the template you have configured in config\packages\scheb_two_factor.yaml:

scheb_two_factor:
    security_tokens:
        template: security/2fa_form.html.twig   # Template used to render the authentication form

Make sure the file is there ;)

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

Ohh, I forgot to add template thought using default you provided. Add now (had to remove logout and cancel link as I have different path). So now it redirects to form but no email process.

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

I have MAILER_DSN set which uses my Mailgun Account in env.

MESSENGER_TRANSPORT_DSN=doctrine://default

messagener:consume command already run on cmd and processing all other emails. Messenger table not updated with any email and no data on command verbose log.

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

This method should be called to send the email. https://github.com/scheb/two-factor-bundle/blob/master/Mailer/AuthCodeMailer.php#L33

Once that method is called, it's the responsibility of your email setup to send-out the mail.

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

Yes, that method gets called. Thank you very much for your help. I will follow documentation for creating Custom Mailer from there I will need to send email using Mailer Interface.

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

Replacing
if ($user instanceof UserInterface)
with
if ($user instanceof UserInterface && $token instanceof UsernamePasswordToken)
redirects properly to /2fa and login works with auth code after implementing custom Mailer service.

Updated event you suggested from
SecurityEvents::INTERACTIVE_LOGIN
to
AuthenticationEvents::AUTHENTICATION_SUCCESS

However, instance of $token is not UsernamePasswordToken but \Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorToken in AuthenticationEvents::AUTHENTICATION_SUCCESS listener.

Hence, my code to update roles for user fails, due to if condition check fail as mentioned above.

Replacing
if ($user instanceof UserInterface && $token instanceof UsernamePasswordToken)
with
if ($user instanceof UserInterface && $token instanceof \Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorToken)
updates UsernamePasswordToken User instance roles but it does not update actual UsernamePasswordToken which I see after login in symfony profiler.

Updated subscriber class:

<?php

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use App\Entity\Permission;
use App\Entity\FrontendConfig;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

class ControllerSubscriber implements EventSubscriberInterface
{
    private $tokenStorage;

    private $em;

    protected $userManager;

    protected $authenticationContextProvider;

    public function __construct(TokenStorageInterface $tokenStorage, 
                                EntityManagerInterface $em, 
                                UserManagerInterface $userManager, 
                                SessionInterface $session)
    {
        $this->tokenStorage = $tokenStorage;
        $this->em = $em;
        $this->userManager = $userManager;
        $this->session = $session;
    }

    public function onKernelController(ControllerEvent $event)
    {
        $frontEndConfig = $this->em->getRepository(FrontendConfig::class)->findAll();
        $config_arr = [];
        foreach($frontEndConfig as $config) {
            $config_arr[$config->getAliasName()] = $config->getValue();
        }

        $this->session->set('frontEndConfig', $config_arr);
    }

    /**
     * @param AuthenticationSuccessEvent $event
     */
    public function onAuthenticationSuccess(AuthenticationSuccessEvent $event)
    {
        $token = $this->tokenStorage->getToken();

        if ($token) {

            $user = $token->getUser();

            if ($user instanceof UserInterface && $token instanceof \Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorToken) {
                //Add all permissions for admin and superadmin users
                $user_roles = $user->getRoles();

                if (in_array('ROLE_ADMIN', $user_roles) || in_array('ROLE_SUPER_ADMIN', $user_roles) || in_array('ROLE_TEACHER', $user_roles)) {

                    if (in_array('ROLE_TEACHER', $user_roles)) {
                        $permissionsNeedToBeMerged = [  'ROLE_CREATE_MEETING', 
                                                        'ROLE_VIEW_AGENDA_MEETINGS', 
                                                        'ROLE_VIEW_AGENDA_HOLIDAYS',
                                                        'ROLE_VIEW_AGENDA_BREAKS'
                                                    ];
                        $user_roles = array_merge($user_roles, $permissionsNeedToBeMerged);
                    } else {
                        $permissions = $this->em->getRepository(Permission::class)->findAll();
                        foreach ($permissions as $permission) {
                            $user_roles[] = $permission->getRole();
                        }
                    }

                    // there didn't seem to be an easier way to grab the provider key, 
                    // so using bound closure to retrieve it
                    $providerKeyGetter = function($token) {
                        return $token->providerKey;
                    };
                    $boundProviderKeyGetter = \Closure::bind($providerKeyGetter, null, $token);

                    // check & load roles for user here if necessary

                    $this->tokenStorage->setToken(
                        new UsernamePasswordToken(
                            $user,
                            $token->getCredentials(),
                            $boundProviderKeyGetter($token), //main == firewall setting
                            array_unique($user_roles)
                        )
                    );
                }
            }
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            ControllerEvent::class => 'onKernelController',
            AuthenticationEvents::AUTHENTICATION_SUCCESS => 'onAuthenticationSuccess',
        ];
    }
}

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

Please use $event->getAuthenticationToken() instead of $this->tokenStorage->getToken(). The one in the token storage is not necessary updated at that point.

And please note that AuthenticationEvents::AUTHENTICATION_SUCCESS is triggered twice. First directly after login (with a TwoFactorToken) and then a second time when 2fa was completed (with the original UsernamePasswordToken). Only on that second call your code should execute.

Btw., the way you're injecting these extra roles, I don't think this is the best way to do it, messing around with the security token. Overwriting the security token is a bit hacky, I believe the security token should only be modified by the security system. The more safe way to do this would probably be to write your own user provider, which adds these extra roles to the user when it's loaded from the database.

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

Thanks for the help and suggestions. Instead of replacing security token, updated User roles with UserManager interface and everything works fine.

                    $user->setRoles(array_unique($user_roles));
                    $this->userManager->updateUser($user);

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

Sounds good to me 👍

from two-factor-bundle.

scheb avatar scheb commented on July 26, 2024

@hardiksinh Can we close this?

from two-factor-bundle.

hardiksinh avatar hardiksinh commented on July 26, 2024

@hardiksinh Can we close this?

Yes

from two-factor-bundle.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.