Oleshek
Oleshek

Reputation: 105

How to implement resend code functionality in scheb/2fa?

I have implemented two-factor authentication based on email code in my app. After user enter his email and password, system redirects him to /2fa route where he need to enter code from email. Everything works great. But I don't understand how to implement resend code functionality. The small advice in doc isn't enough for me. Please help.

Bundle version: scheb/2fa-bundle: 5.10.1, scheb/2fa-email: 5.10.1
Symfony version: 5.2.10
PHP version: 8.0.3

config/packages/security.yaml

# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
    # used to reload user from session & other features (e.g. switch_user)
    app_user_provider:
        entity:
            class: App\Entity\User
            property: email
    # used to reload user from session & other features (e.g. switch_user)
firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    main:
        anonymous: true
        lazy: true
        provider: app_user_provider
        guard:
            authenticators:
                - App\Security\LoginFormAuthenticator
        logout:
            path: app_logout
            # where to redirect after logout
            # target: app_any_route
            
        access_denied_handler: App\Security\AccessDeniedHandler

        two_factor:
            auth_form_path: 2fa_login    # The route name you have used in the routes.yaml
            check_path: 2fa_login_check  # The route name you have used in the routes.yaml
        
        # activate different ways to authenticate
        # https://symfony.com/doc/current/security.html#firewalls-authentication

        # https://symfony.com/doc/current/security/impersonating_user.html
        # switch_user: true

# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
    - { path: '^/2fa', roles: IS_AUTHENTICATED_2FA_IN_PROGRESS }
    - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: '^/logout', roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: '^/register', roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: '^/reset-password', roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: '^/verify/email', roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: '^/', roles: ROLE_USER }
    # - { path: ^/admin, roles: ROLE_ADMIN }
    # - { path: ^/profile, roles: ROLE_USER }

config/packages/scheb_2fa.yaml

scheb_two_factor:
    security_tokens:
         - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
    email:
        enabled: true
        mailer: 2fa_mailer
        template: security/2fa_form.html.twig
        digits: 6

config/routes.yaml

2fa_login:
    path: /2fa
    defaults:
        _controller: "scheb_two_factor.form_controller:form"

2fa_login_check:
    path: /2fa_check

Upvotes: 1

Views: 1179

Answers (2)

Jan
Jan

Reputation: 11

For anybody who needs a complete example, look at this suggestion:

Versions:

  • PHP 8.3
  • Symfony 6.4.8
  • scheb/2fa-bundle 7.4

Symfony Controller:

<?php

namespace App\Controller\Security;

use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Email\Generator\CodeGeneratorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;

class AdminTwoFactorController extends AbstractController
{
    public function __construct(
        private readonly CodeGeneratorInterface $codeGenerator,
        private readonly TokenStorageInterface $tokenStorage,
    ) {
    }

    #[Route('/admin/2fa/resend-code', name: 'admin_2fa_resend_code')]
    public function resendCode(): RedirectResponse
    {
        $token = $this->tokenStorage->getToken();
        $user = $token?->getUser();

        if ($token instanceof TwoFactorTokenInterface && $user instanceof TwoFactorInterface) {
            $this->codeGenerator->generateAndSend($user);
            $this->addFlash('success', 'Code sent.');
        } else {
            $this->addFlash('danger', 'Error sending code.');
        }

        return $this->redirectToRoute('admin_2fa_login');
    }
}

I am using

$this->codeGenerator->generateAndSend($user);

on purpose to not just reSend the code, but to create a new code and send it.

For just resending the existing code, use this:

$this->codeGenerator->reSend($user);

Then I included this link into the 2fa_form.html.twig template, so in case the user did not receive the email, it will be resent.

<a href="{{ path('admin_2fa_resend_code') }}">Send code</a>

My routes/scheb_2fa.yaml file looks like this:

admin_2fa_login:
    path: /admin/2fa/login
    defaults:
        _controller: "scheb_two_factor.form_controller::form"

admin_2fa_login_check:
    path: /admin/2fa/check

And in security.yaml I also added the route at the access_control:

access_control:
 - { path: ^/admin/2fa/resend-code, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }

Hope this helps.

Upvotes: 1

Eugene Kaurov
Eugene Kaurov

Reputation: 2991

To resend a 2-factor-auth-code feel free to inject

Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Email\Generator\CodeGeneratorInterface $codeGenerator 

to your Controller or Service.

After this, you can call:

$codeGenerator->reSend($user)

Feel free to see the documentation: https://github.com/scheb/two-factor-bundle/blob/4.x/Resources/doc/providers/email.md#re-send-authentication-code

Upvotes: 1

Related Questions