Rodolfo Rangel
Rodolfo Rangel

Reputation: 757

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

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.

Upvotes: 19

Views: 15888

Answers (5)

Quentin
Quentin

Reputation: 333

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().

Upvotes: 2

Max Gorovenko
Max Gorovenko

Reputation: 181

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()]);
    }
}

Upvotes: 18

Mucahid CAKMAK
Mucahid CAKMAK

Reputation: 31

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');

Upvotes: 3

Rodolfo Rangel
Rodolfo Rangel

Reputation: 757

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(),
        ]);
    }
}

Upvotes: 17

Julien B.
Julien B.

Reputation: 3334

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()
    ]);
}

Upvotes: 0

Related Questions