Back to blog

Sylius Recipes: Adding Two-Factor Authentication (2FA) to Sylius Admin

Screenshot from 2024-06-24 22-30-05

In today's digital landscape, ensuring the security of your admin panel is paramount. One effective method to enhance security is by implementing Two-Factor Authentication (2FA). This extra layer of security significantly reduces the risk of unauthorized access, even if login credentials are compromised.

Follow the steps below to implement Two-Factor Authentication in Sylius Admin using the email authentication method. I'll tell you how to add new methods at the end of this article.

Step 1: Adding the Scheb Symfony packages

All the 2FA logic comes with the fantastic Symfony packages from Scheb. We need to add them to the project:

$ composer require scheb/2fa-bundle scheb/2fa-email scheb/2fa-trusted-device

If you are not using Symfony Flex, be sure to add this line to the config/bundles.php:

<?php
return [
    // ...
    Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
];

Step 2: Configure the Scheb Bundle

You can adjust this configuration based on your needs.

# config/packages/scheb_2fa.yaml
scheb_two_factor:
    trusted_device:
        enabled: true
        lifetime: 2592000
    email:
        enabled: true
        digits: 6
        mailer: App\Mailer\TwoFactorAuthCodeEmailManager
        template: admin/security/2fa_form.html.twig
    security_tokens:
        - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
        - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken

Now, add the following lines to the config/packages/security.yaml file:

# config/packages/security.yaml
security:
    # ...
    firewalls:
        admin:
            # ...
            two_factor:
                auth_form_path: app_admin_2fa_login
                check_path: app_admin_2fa_login_check
    access_control:
        # ...
        - { path: "%sylius.security.admin_regex%/2fa", role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
        - { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS }
        # ...

Finally, replace the content of the config/routes/scheb_2fa.yaml file with this:

# config/routes/scheb_2fa.yaml
app_admin_2fa_login:
    path: '/%sylius_admin.path_name%/2fa'
    defaults:
        _controller: "scheb_two_factor.form_controller::form"

app_admin_2fa_login_check:
    path: '/%sylius_admin.path_name%/2fa/check'

Step 3: Configuring Sylius Mailer to send the email with the Auth Code

# config/packages/_sylius.yaml
# ...
sylius_mailer:
    sender:
        name: Odiseo
        address: team@odiseo.com.ar
    emails:
        two_factor_auth_code:
            subject: app.emails.two_factor_auth_code.subject
            template: "common/email/2fa_auth_code.html.twig"

Step 4: Creating the email manager

Now we need to create the service that uses the Sylius Mailer to send the auth code by email. Create this class in src/Mailer/TwoFactorAuthCodeEmailManager.php:

<?php

declare(strict_types=1);

namespace App\Mailer;

use Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface;
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Locale\Context\LocaleContextInterface;
use Sylius\Component\Mailer\Sender\SenderInterface;

final class TwoFactorAuthCodeEmailManager implements AuthCodeMailerInterface
{
    private const TWO_FACTOR_AUTH_CODE = 'two_factor_auth_code';

    public function __construct(
        private SenderInterface $emailSender,
        private ChannelContextInterface $channelContext,
        private LocaleContextInterface $localeContext
    ) {
    }

    public function sendAuthCode(TwoFactorInterface $user): void
    {
        /** @var ChannelInterface $channel */
        $channel = $this->channelContext->getChannel();
        $localeCode = $this->localeContext->getLocaleCode();

        $this->emailSender->send(
            self::TWO_FACTOR_AUTH_CODE,
            [$user->getEmailAuthRecipient()],
            [
                'authCode' => $user->getEmailAuthCode(),
                'channel' => $channel,
                'localeCode' => $localeCode,
            ],
        );
    }
}

Step 5: Creating the form and email templates

Create the twig template to render the two factor auth form in templates/admin/security/2fa_form.html.twig:

{% extends '@SyliusUi/Layout/centered.html.twig' %}

{% import '@SyliusUi/Macro/messages.html.twig' as messages %}

{% block title %}{{ parent() }} | {{ 'app.ui.two_factor_auth'|trans }}{% endblock %}

{% block stylesheets %}
    {{ sylius_template_event('sylius.admin.layout.stylesheets') }}
{% endblock %}

{% block content %}
    <div class="ui middle aligned center aligned grid">
        <div class="column">
            <div style="max-width: 270px; margin: 0 auto; margin-bottom: 40px;">
                <img src="{{ asset('build/admin/images/logo.png', 'admin') }}" class="ui fluid image" alt="Logo">
            </div>

            {% if authenticationError %}
                <div class="ui left aligned basic segment">
                    {{ messages.error(authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle')) }}
                </div>
            {% endif %}

            <form class="ui large loadable form" action="{{ checkPathUrl ? checkPathUrl: path(checkPathRoute) }}" method="post" novalidate>
                <div class="ui left aligned very padded segment">
                    <div class="required field">
                        <label for="_auth_code">{{ "auth_code"|trans({}, 'SchebTwoFactorBundle') }} {{ twoFactorProvider }}:</label>
                        <input
                            id="_auth_code"
                            type="text"
                            name="{{ authCodeParameterName }}"
                            autocomplete="one-time-code"
                            autofocus
                        />
                    </div>

                    {% if displayTrustedOption %}
                        <div class="field">
                            <div class="ui toggle checkbox">
                                <input id="_trusted" type="checkbox" name="{{ trustedParameterName }}" />
                                <label for="_trusted">{{ "trusted"|trans({}, 'SchebTwoFactorBundle') }}</label>
                            </div>
                        </div>
                    {% endif %}

                    {% if isCsrfProtectionEnabled %}
                        <input type="hidden" name="{{ csrfParameterName }}" value="{{ csrf_token(csrfTokenId) }}">
                    {% endif %}

                    <button type="submit" class="ui fluid large primary submit button">{{ "login"|trans({}, 'SchebTwoFactorBundle') }}</button>

                    <div class="cancel" style="text-align: center; margin-top: 1em;">
                        <a href="{{ logoutPath }}">{{ "cancel"|trans({}, 'SchebTwoFactorBundle') }}</a>
                    </div>
                </div>
            </form>
        </div>
    </div>
{% endblock %}

{% block javascripts %}
    {{ sylius_template_event('sylius.admin.layout.javascripts') }}
{% endblock %}

Now the twig email template in templates/common/email/2fa_auth_code.html.twig:

{% extends '@SyliusCore/Email/layout.html.twig' %}

{% block subject %}
    {{ 'app.email.two_factor_auth_code.subject'|trans({}, null, localeCode) }}
{% endblock %}

{% block content %}
    <div style="text-align: center; margin-bottom: 30px;">
        {{ 'app.email.two_factor_auth_code.your_code'|trans({}, null, localeCode) }}:
        <div style="margin: 20px 0;">
            <span style="border: 1px solid #1abb9c; padding: 10px; color: #1abb9c; font-size: 28px;">
                {{ authCode }}
            </span>
        </div>
    </div>
{% endblock %}

And don't forget to add the necessary translation keys:

# translations/messages.en.yaml
app:
    email:
        two_factor_auth_code:
            subject: Two factor auth code
            your_code: To complete the sign in, enter the following verification code

Step 6: Adding the auth code field to the Sylius Admin User entity

// src/Entity/User/AdminUser.php
<?php

declare(strict_types=1);

namespace App\Entity\User;

use Doctrine\ORM\Mapping as ORM;
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
use Sylius\Component\Core\Model\AdminUser as BaseAdminUser;

#[ORM\Entity]
#[ORM\Table(name: 'sylius_admin_user')]
class AdminUser extends BaseAdminUser implements TwoFactorInterface
{
    #[ORM\Column(name: 'auth_code', type: 'string', nullable: true)]
    private ?string $authCode;
    
    public function isEmailAuthEnabled(): bool
    {
        return true; // This can be a persisted field to switch email code authentication on/off
    }

    public function getEmailAuthRecipient(): string
    {
        return $this->email;
    }

    public function getEmailAuthCode(): string
    {
        if (null === $this->authCode) {
            throw new \LogicException('The email authentication code was not set');
        }

        return $this->authCode;
    }

    public function setEmailAuthCode(string $authCode): void
    {
        $this->authCode = $authCode;
    }
}

Remember to create and execute the migrations:

$ php bin/console doctrine:migrations:diff
$ php bin/console doctrine:migrations:migrate

Now you can enter to the Sylius Admin login page and test the 2FA integration!

Further reading

  • You can customize the integration based on your project's needs. For example you can edit the twig templates adding more relevant information or the design.
  • You can add more authentication methods such as SMS, Google Authenticator and more following the well documented Scheb bundle: https://symfony.com/bundles/SchebTwoFactorBundle/6.x/index.html.
  • You can ask for help or another Symfony / Sylius request sending us an email here: team@odiseo.com.ar or using our contact page.
  • Stay tuned for more Sylius recipes to enhance your e-commerce platform's functionality following us on LinkedIn.
Share this Post:
Diego D'amico
Football and technology lover. I'm 39 years old, I'm married with 2 children and 2 dogs.
I constantly look ways to improve and progress both personally and professionally