14

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.

Rodolfo Rangel
  • 596
  • 1
  • 3
  • 15
  • 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. – Cerad Apr 03 '21 at 13:02
  • 1
    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 go – Cerad Apr 03 '21 at 13:02

5 Answers5

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(),
        ]);
    }
}
Cerad
  • 48,157
  • 8
  • 90
  • 92
Rodolfo Rangel
  • 596
  • 1
  • 3
  • 15
  • 3
    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. – Cerad Apr 04 '21 at 12:41
10

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()]);
    }
}
Max Gorovenko
  • 101
  • 1
  • 2
  • I used this solution and it's working like a charm. Thanks! – Ajie62 May 15 '22 at 12:10
  • 3
    for activation Remember Me need enable badge: $rememberMe = new RememberMeBadge(); $rememberMe->enable(); $authenticatorManager->authenticateUser($user, $this->authenticator, $request, [$rememberMe]); – dubenko May 18 '22 at 10:06
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');

Kaaveh Mohamedi
  • 1,399
  • 1
  • 15
  • 36
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().

Darryl Hein
  • 142,451
  • 95
  • 218
  • 261
Quentin
  • 183
  • 1
  • 15
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()
    ]);
}
Julien B.
  • 3,023
  • 2
  • 18
  • 33
  • 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. – Rodolfo Rangel Apr 03 '21 at 05:28