Back to the blog

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

Sylius

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
Next step

Have a project in mind?

Tell us what you are building.

Keep reading

All articles
Sylius eCommerce AI

Why we're building a shopping agent for Sylius

We're building a shopping agent for Sylius in the open. It runs inside your store, using your own API key, and is based on Anthropic's reference implementation for commerce agents. This post covers what it can do, how it stays accurate and what already works.