How to manually authenticate user after Registration with the new Symfony 5 Authenticator?
Asked Answered
C

5

17

Symfony 5 has changed its guard authentication method to a new Passport based one, using the new security config: enable_authenticator_manager: true;

I would like to know how to authenticate a user in the Registration form method in my controller, after the user is persisted by the ORM (Doctrine);

I have succeeded in authenticating the user using the login form, but I still do not know how to manually do this.

Chellean answered 2/4, 2021 at 20:33 Comment(2)
Good question. Wish I had an answer for you. I don't think there is a standard way yet. bin/console make:registration-form does not yet handle the new authenticators. Might try over on the Symfony slack channel. You could try calling AuthenticateManager::authenticateUser or even duplicating some of the code in AuthenticateManager::executeAuthenticator. But I suspect you might just have to wait until things settle down. Remember the new stuff is still experimental. Let us know if you get it working.Motivate
Just wanted to add that the comments for UserAuthenticatorInterface::authenticateUser say: "Convenience method to programmatically login a user and return a Response if any for success." So that seems to be the way to goMotivate
C
16

As per Cerad's comment, here is the full answer.

Below is only the part of the code related to the question & answer. These are not the full files.

Also, this is only for Symfony ^5.2 that is not using guard to authenticate the user.

/* config/packages/security.yaml */

security:
    enable_authenticator_manager: true
    firewalls:
        main:
            custom_authenticators:
                - App\Security\SecurityAuthenticator
/* src/Security/SecurityAuthenticator.php */

use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;

/* automatically generated with the make:auth command,
     the important part is to undestand that this is not a Guard implement 
     for the Authenticator class */
class SecurityAuthenticator extends AbstractLoginFormAuthenticator
{
  
}
/* src/Controller/RegistrationController.php */

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\SecurityAuthenticator;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;

class RegistrationController extends AbstractController
{

    /**
     * @Route("/register", name="app_register")
     */
    public function register(
        Request $request, 
        UserPasswordEncoderInterface $passwordEncoder, 
        UserAuthenticatorInterface $authenticator, 
        SecurityAuthenticator $formAuthenticator): Response
    {
      /* Automatically generated by make:registration-form, but some changes are
         needed, like the auto-wiring of the UserAuthenticatorInterface and 
         SecurityAuthenticator */
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // encode the plain password
            $user->setPassword($passwordEncoder->encodePassword($user, $form->get('password')->getData()));

            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($user);
            $entityManager->flush();

            // substitute the previous line (redirect response) with this one.
            return $authenticator->authenticateUser(
                $user, 
                $formAuthenticator, 
                $request); 
        }

        return $this->render('registration/register.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }
}
Chellean answered 3/4, 2021 at 22:15 Comment(1)
Good answer. I was wondering how to get the user authenticator for the current firewall. Never occurred to me to just typehint against it. The user authenticator is actually a security bundle class which determines the current firewall based on the master request. Good stuff to know.Motivate
Y
17

For Symfony 6 find working solution, based on @Cerad's comment about UserAuthenticatorInterface::authenticateUser().

I declared my RegisterController in services.yaml with important argument (it is the reason):

App\Controller\RegisterController:
    arguments:
        $authenticator: '@security.authenticator.form_login.main'

So my RegisterController now looks like:

class RegisterController extends AbstractController
{
    public function __construct(
        private FormLoginAuthenticator $authenticator
    ) {
    }

    #[Route(path: '/register', name: 'register')]
    public function register(
        Request $request,
        UserAuthenticatorInterface $authenticatorManager,
    ): RedirectResponse|Response {
        // some create logic
        ...

        // auth, not sure if RememberMeBadge works, keep testing
        $authenticatorManager->authenticateUser($user, $this->authenticator, $request, [new RememberMeBadge()]);
    }
}
Yul answered 19/1, 2022 at 14:11 Comment(3)
I used this solution and it's working like a charm. Thanks!Dustpan
for activation Remember Me need enable badge: $rememberMe = new RememberMeBadge(); $rememberMe->enable(); $authenticatorManager->authenticateUser($user, $this->authenticator, $request, [$rememberMe]);Gagliano
One-liner to force remember me with [(new RememberMeBadge())->enable()]. Also when using custom_authenticators be sure to enable remember_me in your security.firewalls.[name] otherwise the cookie will not be saved. Lastly, calling ::enable() isn't needed if you set always_remember_me: trueMaunder
C
16

As per Cerad's comment, here is the full answer.

Below is only the part of the code related to the question & answer. These are not the full files.

Also, this is only for Symfony ^5.2 that is not using guard to authenticate the user.

/* config/packages/security.yaml */

security:
    enable_authenticator_manager: true
    firewalls:
        main:
            custom_authenticators:
                - App\Security\SecurityAuthenticator
/* src/Security/SecurityAuthenticator.php */

use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;

/* automatically generated with the make:auth command,
     the important part is to undestand that this is not a Guard implement 
     for the Authenticator class */
class SecurityAuthenticator extends AbstractLoginFormAuthenticator
{
  
}
/* src/Controller/RegistrationController.php */

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\SecurityAuthenticator;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;

class RegistrationController extends AbstractController
{

    /**
     * @Route("/register", name="app_register")
     */
    public function register(
        Request $request, 
        UserPasswordEncoderInterface $passwordEncoder, 
        UserAuthenticatorInterface $authenticator, 
        SecurityAuthenticator $formAuthenticator): Response
    {
      /* Automatically generated by make:registration-form, but some changes are
         needed, like the auto-wiring of the UserAuthenticatorInterface and 
         SecurityAuthenticator */
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // encode the plain password
            $user->setPassword($passwordEncoder->encodePassword($user, $form->get('password')->getData()));

            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($user);
            $entityManager->flush();

            // substitute the previous line (redirect response) with this one.
            return $authenticator->authenticateUser(
                $user, 
                $formAuthenticator, 
                $request); 
        }

        return $this->render('registration/register.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }
}
Chellean answered 3/4, 2021 at 22:15 Comment(1)
Good answer. I was wondering how to get the user authenticator for the current firewall. Never occurred to me to just typehint against it. The user authenticator is actually a security bundle class which determines the current firewall based on the master request. Good stuff to know.Motivate
H
3

Symfony 5.3 it's work for me

public function register(Request $request, Security $security, UserPasswordEncoderInterface $passwordEncoder, EventDispatcherInterface $dispatcher) {


......

$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->get("security.token_storage")->setToken($token);

$event = new SecurityEvents($request);
$dispatcher->dispatch($event, SecurityEvents::INTERACTIVE_LOGIN);
return $this->redirectToRoute('home');

Hettiehetty answered 20/12, 2021 at 14:9 Comment(1)
doesnt auto log in in 6.1Megdal
F
2

Here's my go at it, allowing you to authenticate a user, and also attach attributes to the generated token:

// src/Service/UserService.php
<?php

namespace App\Service;

use App\Entity\User;
use App\Security\LoginFormAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class UserService
{

    private AuthenticatorInterface $authenticator;
    private TokenStorageInterface $tokenStorage;
    private EventDispatcherInterface $eventDispatcher;

    // This (second parameter) is where you specify your own authenticator,
    // if you have defined one; or use the built-in you're using
    public function __construct(
        LoginFormAuthenticator $authenticator,
        TokenStorageInterface $tokenStorage,
        EventDispatcherInterface $eventDispatcher
    ) {
        $this->authenticator = $authenticator;
        $this->tokenStorage = $tokenStorage;
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * @param User $user
     * @param Request $request
     * @param ?array $attributes
     * @return ?Response
     *
     */
    public function authenticate(User $user, Request $request, array $attributes = []): ?Response
    {
        $firewallName = 'main';

        /** @see AuthenticatorManager::authenticateUser() */

        $passport = new SelfValidatingPassport(
            new UserBadge($user->getUserIdentifier(), function () use ($user) {
                return $user;
            })
        );

        $token = $this->authenticator->createAuthenticatedToken($passport, $firewallName);
        /** @var TokenInterface $token */
        $token = $this->eventDispatcher->dispatch(
            new AuthenticationTokenCreatedEvent($token, $passport)
        )->getAuthenticatedToken();

        $token->setAttributes($attributes);

        /** @see AuthenticatorManager::handleAuthenticationSuccess() */

        $this->tokenStorage->setToken($token);
        $response = $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);

        if ($this->authenticator instanceof InteractiveAuthenticatorInterface && $this->authenticator->isInteractive()) {
            $loginEvent = new InteractiveLoginEvent($request, $token);
            $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
        }

        $this->eventDispatcher->dispatch(
            $loginSuccessEvent = new LoginSuccessEvent(
                $this->authenticator,
                $passport,
                $token,
                $request,
                $response,
                $firewallName
            )
        );

        return $loginSuccessEvent->getResponse();
    }

}

Largely inspired from AuthenticatorManager::authenticateUser() and AuthenticatorManager::handleAuthenticationSuccess().

Fingerboard answered 16/11, 2021 at 14:2 Comment(0)
P
0

This might work depending on your setup. Note that main in the authenticateUserAndHandleSuccess() method is the name of my firewall in config/packages/security.yaml and LoginFormAuthenticator is the authenticator I created using bin/console make:auth.

/**
 * @Route("/register", name="app_register")
 * @param Request                      $request
 * @param EntityManagerInterface       $entityManager
 * @param GuardAuthenticatorHandler    $handler
 * @param LoginFormAuthenticator       $authenticator
 * @param UserPasswordEncoderInterface $encoder
 *
 * @return Response
 */
public function register(
    Request $request, EntityManagerInterface $entityManager, GuardAuthenticatorHandler $handler,
    LoginFormAuthenticator $authenticator, UserPasswordEncoderInterface $encoder
): Response {
    $user = new User();
    $form = $this->createForm(RegisterType::class, $user);

    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $plainPassword = $form->get('plainPassword')->getData();
        $user->setPassword($encoder->encodePassword($user, $plainPassword));

        $entityManager->persist($user);
        $entityManager->flush();

        $handler->authenticateUserAndHandleSuccess($user, $request, $authenticator, 'main');
    }

    return $this->render('security/register.html.twig', [
        'form' => $form->createView()
    ]);
}
Possibility answered 3/4, 2021 at 3:40 Comment(1)
I have tried this solution and unfortunately it does not work because the third argument of authenticateUserAndHandleSuccess() must implement 'Symfony\Component\Security\Guard\AuthenticatorInterface' and my Authenticator class extends the new 'Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator', which in turn implements a whole lot of other classes, but none of them are from the 'Guard' based approach.Chellean

© 2022 - 2024 — McMap. All rights reserved.