Search code examples
symfonyauthenticationsymfony4symfony-security

Symfony 4 Security - how to configure a custom Guard authenticator?


I have setup my own login form in a security controller. to use the LoginForm I have configured this in the security configuration. I want to use a custom login form authenticator to have more control over the authentication progress, register logins in the system, and do anything I'd like to add (IP-check etc, etc...)

So there is also a LoginFormAuthenticator class in my application. somehow the authentication process doesn't even seem to use the methods of the custom LoginFormAuthenticator. Is my security.yaml configured properly? how do I get all my configuration to work together?

The security in symfony seems so messy at some points, I can't begin to understand how people manage to properly configure it..

LoginFormAuthenticator:

class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
    /**
     * Constructor
     *
     * @param Logger                       $logger
     * @param LoginAttemptManagerInterface $loginAttemptManager
     * @param LocationManagerInterface     $locationManager
     * @param RouterInterface              $router
     * @param UserPasswordEncoderInterface $userPasswordEncoder
     * @param UserRepositoryInterface      $userRepository
     */
    public function __construct(Logger $logger, LoginAttemptManagerInterface $loginAttemptManager, LocationManagerInterface $locationManager, RouterInterface $router, UserPasswordEncoderInterface $userPasswordEncoder, UserRepositoryInterface $userRepository)
    {
        $this->_logger              = $logger;
        $this->_loginAttemptManager = $loginAttemptManager;
        $this->_locationManager     = $locationManager;
        $this->_router              = $router;
        $this->_userPasswordEncoder = $userPasswordEncoder;
        $this->_userRepository      = $userRepository;
    }

    /**
     * {@inheritdoc}
     */
    protected function getLoginUrl()
    {
        return $this->_router->generate("login");
    }

    /**
     * {@inheritdoc}
     */
    public function getCredentials(Request $request)
    {
        $credentials = $request->get("login_form");

        return [
            "username" => $credentials["username"],
            "password" => $credentials["password"],
            "token"    => $credentials["_token"],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $username = $credentials["username"];

        try {
            $user = $this->_userRepository->findOneByUsername($username);

            if (null !== $user && $user instanceof UserInterface) {
                /* @var LoginAttempt $loginAttempt */
                $loginAttempt = $this->_loginAttemptManager->create();

                $user->addLoginAttempt($loginAttempt);
            }
        }
        catch (NoResultException $e) {
            return null;
        }
        catch (NonUniqueResultException $e) {
            return null;
        }
        catch (UsernameNotFoundException $e) {
            return null;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function checkCredentials($credentials, UserInterface $user)
    {
        /* @var string $rawPassword the unencoded plain password */
        $rawPassword = $credentials["password"];

        if ($this->_userPasswordEncoder->isPasswordValid($user, $rawPassword)) {
            return true;
        }

        return new CustomUserMessageAuthenticationException("Invalid credentials");
    }

    /**
     * {@inheritdoc}
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        /* @var AbstractUser $user */
        $user = $token->getUser();

        /* @var LoginAttempt $loginAttempt */
        $loginAttempt = $user->getLastLoginAttempt();

        $loginAttempt->success();

        this->_loginAttemptManager->saveOne($loginAttempt, true);
    }

    /**
     * {@inheritdoc}
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        // without this method the authentication process becomes a loop
    }

    /**
     * {@inheritdoc}
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse($this->getLoginUrl());
    }

    /**
     * {@inheritdoc}
     */
    public function supports(Request $request)
    {
        return $request->getPathInfo() != $this->getLoginUrl() || !$request->isMethod(Request::METHOD_POST);
    }

    /**
     * {@inheritdoc}
     */
    public function supportsRememberMe()
    {
        return true;
    }
}

SecurityController:

class SecurityController extends AbstractController
{
    /**
     * @Route(path = "login", name = "login", methods = {"GET", "POST"})
     * @Template(template = "security/login.html.twig")
     *
     * @param AuthenticationUtils $authUtils
     * @param Request             $request
     * @return array
     */
    public function login(AuthenticationUtils $authUtils, Request $request)
    {
        $form = $this->createLoginForm();

        if (null !== $authUtils->getLastAuthenticationError()) {
            $form->addError(new FormError(
                $this->_translator->trans("error.authentication.incorrect-credentials", [], "security")
            ));
        }

        if (null != $authUtils->getLastUsername()) {
            $form->setData([
                "username" => $authUtils->getLastUsername(),
            ]);
        }

        // settings are in config/packages/security.yaml
        // configuration authenticates user in login form authenticator service

        return [
            "backgroundImages" => $this->_backgroundImageManager->findAll(),
            "form"             => $form->createView(),
        ];
    }

    /**
     * @return FormInterface
     */
    private function createLoginForm() : FormInterface
    {
        $form = $this->createForm(LoginForm::class, null, [
            "action"    => $this->generateUrl("login"),
            "method"    => Request::METHOD_POST,
        ]);

        $form->add("submit", SubmitType::class, [
            "label"              => $this->_translator->trans("btn.login", [], "button"),
            "icon_name"          => "sign-in",
            "translation_domain" => false,
        ]);

        return $form;
    }
}

security.yaml:

security:
    providers:

        user_provider:
            entity:
                class: App\Entity\Model\AbstractUser
                property: username

        oauth_provider:
            entity:
                class: App\Entity\Model\ApiClient
                property: name

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        # The API-Oauth-Token-Firewall must be above the API-firewall
        api_oauth_token:
            pattern: ^/api/oauth/token$
            security: false

        # The API-firewall must be above the Main-firewall
        api:
            pattern: ^/api/*
            security: true
            stateless: true
            oauth2: true
            provider: oauth_provider
            access_denied_handler: App\Service\Api\Security\ApiAccessDeniedHandler

        main:
            anonymous: true
            guard:
                authenticators:
                    - App\Service\Security\LoginFormAuthenticator

            access_denied_handler: App\Service\Security\AccessDeniedHandler

            provider: user_provider

            form_login:
                login_path: /login
                check_path: /login
                default_target_path: / #index

                username_parameter: "login_form[username]"
                password_parameter: "login_form[password]"

            logout:
                # the logout path overrides the implementation of the logout method
                # in the security controller
                path: /logout
                target: / #index

            remember_me:
                secret: '%kernel.secret%'
                lifetime: 43200 # 60 sec * 60 min * 12 hours
                path: /
                remember_me_parameter: "login_form[remember]"

    encoders:
        App\Entity\Model\AbstractUser:
            algorithm: bcrypt
            cost: 13

    access_control:
        # omitted from this question

    role_hierarchy:
        # omitted from this question

Solution

  • Apparently I had two forms of authentication configured in security.yaml

    So I've removed the form_login key from the config:

    security.yaml

    security:
        providers:
    
            user_provider:
                entity:
                    class: App\Entity\Model\AbstractUser
                    property: username
    
            oauth_provider:
                entity:
                    class: App\Entity\Model\ApiClient
                    property: name
    
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
    
            # The API-Oauth-Token-Firewall must be above the API-firewall
            api_oauth_token:
                pattern: ^/api/oauth/token$
                security: false
    
            # The API-firewall must be above the Main-firewall
            api:
                pattern: ^/api/*
                security: true
                stateless: true
                oauth2: true
                provider: oauth_provider
                access_denied_handler: App\Service\Api\Security\ApiAccessDeniedHandler
    
            main:
                anonymous: true
                guard:
                    authenticators:
                        - App\Service\Security\LoginFormAuthenticator
    
                access_denied_handler: App\Service\Security\AccessDeniedHandler
    
                provider: user_provider
    
                logout:
                    # the logout path overrides the implementation of the logout method
                    # in the security controller
                    path: /logout
                    target: / #index
    
                remember_me:
                    secret: '%kernel.secret%'
                    lifetime: 43200 # 60 sec * 60 min * 12 hours
                    path: /
                    remember_me_parameter: "login_form[remember]"
    

    And updated the LoginFormAuthenticator - integrated - also added checking CSRF token

    LoginFormAuthenticator

    class LoginFormAuthenticator extends AbstractGuardAuthenticator
    {
        const FORM       = "login_form";
        const USERNAME   = "username";
        const PASSWORD   = "password";
        const CSRF_TOKEN = "token";
    
        /**
         * Constructor
         *
         * @param CsrfTokenManagerInterface    $csrfTokenManager
         * @param Logger                       $logger
         * @param LoginAttemptManagerInterface $loginAttemptManager
         * @param LocationManagerInterface     $locationManager
         * @param RouterInterface              $router
         * @param UserPasswordEncoderInterface $userPasswordEncoder
         * @param UserRepositoryInterface      $userRepository
         */
        public function __construct(CsrfTokenManagerInterface $csrfTokenManager, Logger $logger, LoginAttemptManagerInterface $loginAttemptManager, LocationManagerInterface $locationManager, RouterInterface $router, UserPasswordEncoderInterface $userPasswordEncoder, UserRepositoryInterface $userRepository)
        {
            $this->_csrfTokenManager    = $csrfTokenManager;
            $this->_logger              = $logger;
            $this->_loginAttemptManager = $loginAttemptManager;
            $this->_locationManager     = $locationManager;
            $this->_router              = $router;
            $this->_userPasswordEncoder = $userPasswordEncoder;
            $this->_userRepository      = $userRepository;
        }
    
        /**
         * Get Login URL
         *
         * @return string
         */
        protected function getLoginUrl()
        {
            return $this->_router->generate("login");
        }
    
        /**
         * Get Target URL
         *
         * @return string
         */
        protected function getTargetUrl()
        {
            return $this->_router->generate("index");
        }
    
        /**
         * {@inheritdoc}
         */
        public function getCredentials(Request $request)
        {
            $credentials = $request->request->get(self::FORM);
    
            $request->getSession()->set(Security::LAST_USERNAME, $credentials["username"]);
    
            return [
                self::USERNAME   => $credentials["username"],
                self::PASSWORD   => $credentials["password"],
                self::CSRF_TOKEN => $credentials["_token"],
            ];
        }
    
        /**
         * {@inheritdoc}
         */
        public function getUser($credentials, UserProviderInterface $userProvider)
        {
            $username = $credentials[self::USERNAME];
    
            try {
                $user = $this->_userRepository->findOneByUsername($username);
    
                if (null !== $user && $user instanceof UserInterface) {
                    /* @var LoginAttempt $loginAttempt */
                    $loginAttempt = $this->_loginAttemptManager->create();
    
                    $user->addLoginAttempt($loginAttempt);
                }
    
                return $user;
            }
            catch (NoResultException $e) {
                throw new BadCredentialsException("Authentication failed");
            }
            catch (NonUniqueResultException $e) {
                throw new BadCredentialsException("Authentication failed");
            }
        }
    
        /**
         * {@inheritdoc}
         */
        public function checkCredentials($credentials, UserInterface $user)
        {
            $csrfToken = new CsrfToken(self::FORM, $credentials[self::CSRF_TOKEN]);
    
            if (false === $this->_csrfTokenManager->isTokenValid($csrfToken)) {
                throw new InvalidCsrfTokenException('Invalid CSRF token');
            }
    
            /* @var string $rawPassword the unencoded plain password */
            $rawPassword = $credentials[self::PASSWORD];
    
            if ($this->_userPasswordEncoder->isPasswordValid($user, $rawPassword)) {
                return true;
            }
    
            /* @var AbstractUser $user */
            $loginAttempt = $user->getLastLoginAttempt();
    
            if (null !== $loginAttempt) {
                $this->_loginAttemptManager->saveOne($loginAttempt);
            }
    
            return new CustomUserMessageAuthenticationException("Invalid credentials");
        }
    
        /**
         * {@inheritdoc}
         */
        public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
        {
            /* @var AbstractUser $user */
            $user = $token->getUser();
    
            /* @var LoginAttempt $loginAttempt */
            $loginAttempt = $user->getLastLoginAttempt();
    
            $loginAttempt->setStatus(LoginAttempt::STATUS_AUTHENTICATION_SUCCESS);
    
            if (null !== $loginAttempt) {
                $this->_loginAttemptManager->saveOne($loginAttempt);
            }
    
            return new RedirectResponse($this->getTargetUrl());
        }
    
        /**
         * {@inheritdoc}
         */
        public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
        {
            $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception->getMessage());
    
            return new RedirectResponse($this->getLoginUrl());
        }
    
        /**
         * {@inheritdoc}
         */
        public function start(Request $request, AuthenticationException $authException = null)
        {
            return new RedirectResponse($this->getLoginUrl());
        }
    
        /**
         * {@inheritdoc}
         */
        public function supports(Request $request)
        {
            return $request->getPathInfo() === $this->getLoginUrl() && $request->isMethod(Request::METHOD_POST);
        }
    
        /**
         * {@inheritdoc}
         */
        public function supportsRememberMe()
        {
            return true;
        }
    }