Enhancing Sylius Security with Two-Factor Authentication (2FA) Using TOTP

Enhancing Sylius Security with Two-Factor Authentication (2FA) Using TOTP

Introduction

In order to enhance the security of your Sylius webshop, we’re diving into the realm of two-factor authentication (2FA). This time, we’re adopting the scheb/2fa-bundle bundle with the scheb/2fa-totp extension to seamlessly integrate Time-based One-Time Password (TOTP) authentication.

While I’m here to guide you through the basics, the details are left as an exercise for the reader (that’s you!).

Environment setup

I’ll assume you have a sylius skeleton application up and running. For this, you can check out the Sylius Docs.

Installing the bundle

Let’s start things off by installing the bundle and extension that we will be using.

composer require scheb/2fa-bundle
composer require scheb/2fa-totp

The Flex recipe for the bundle brings in default configurations. We’ll adjust these to meet our specific needs.

Customizing the bundle

Post-installation, a couple of files appear, namely:

config/packages/scheb_2fa.yaml
config/routes/scheb_2fa.yaml

We will modify the package configuration file to suit our needs.

# config/packages/scheb_2fa.yaml
scheb_two_factor:
    security_tokens:
        - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
    totp:
        enabled: true

Here we’re enabling the TOTP extension, and configuring which security token needs to trigger the two-factor authentication. This is the token used by the Sylius Admin authentication.

Next up, tweak the routes in the routing file:

# config/routes/scheb_2fa.yaml
2fa_login:
    path: /admin/2fa
    defaults:
        _controller: "scheb_two_factor.form_controller::form"

2fa_login_check:
    path: /admin/2fa_check

We simply prepend the /admin prefix to both routes.

Adjusting the firewall config

We’ll have to make some adjustments to the Firewall configuration. Update the admin firewall configuration by introducing the two_factor key:

# config/packages/security.yaml
security:
    firewalls:
        admin:
            # The default configuration remains here, we just add the following
            two_factor:
                auth_form_path: /admin/2fa
                check_path: /admin/2fa_check
                post_only: true
                default_target_path: /admin

    access_control:
        # We also need to expose the 2fa routes. For this, there's a custom role available.
        - { path: "%sylius.security.admin_regex%/2fa", role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
        - { path: "%sylius.security.admin_regex%/2fa_check", role: IS_AUTHENTICATED_2FA_IN_PROGRESS }

This provides the security component with the necessary information to handle the two-factor authentication. We can also see that we exposed the 2fa routes to the IS_AUTHENTICATED_2FA_IN_PROGRESS role. This role is automatically assigned to the user when the two-factor authentication is triggered.

2FA Authentication flow diagram

Adjusting the user entity

The AdminUser entity should implement the TwoFactorInterface. With this interface, come a few functions we need to implement.

# src/Entity/User/AdminUser.php
class AdminUser extends BaseAdminUser implements TwoFactorInterface
{
    /**
    * @ORM\Column(type="string", nullable=true)
    */
    protected ?string $totpSecret;

    public function isTotpAuthenticationEnabled(): bool
    {
        return $this->totpSecret !== null && $this->totpSecret !== '';
    }

    public function getTotpAuthenticationUsername(): string
    {
        return $this->username;
    }

    public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
    {
        return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 20, 8);
    }
}

We added a new column to the user entity, namely totpSecret. This will hold the secret key for the user.

isTotpAuthenticationEnabled

This function should return true if the user has enabled 2fa authentication. In this case, we simply check if the totpSecret is not null or empty. You could map this to a boolean field in your database, or any other way you see fit.

getTotpAuthenticationUsername

This function should return the username of the user.

getTotpAuthenticationConfiguration

This function should return the Totp configuration. Here you can adjust certain parameters, like the algorithm, the number of digits, etc.

Testing the functionality

With everything set up, test the functionality after running Sylius fixtures. The demo admin user won’t prompt for 2FA authentication initially, as the user lacks a secret key.

The implementation of providing the user with a secret, and a QR code to scan, is up to you. For this example, I simply generate it manually, and add it to the user in the database.

To generate a secret and the QR code content, utilize the TotpAuthenticatorInterface service:

// Generate secret
$this->totpAuthenticator->generateSecret();

// Generate QR code content
$this->totpAuthenticator->getQRContent($user);

Turning this content into a QR code should always be done in your application, and not by a 3rd party service. As this would expose the secret key to that service.

After manually setting the secret on the user in the database, attempting to log in to the Sylius admin section will display the TOTP authentication screen.

While the default styling might need customization, the functionality is in place. After configuring your Google Authenticator app, use the generated code to securely log in.

TOTP Screen

Conclusion

Integrating TOTP authentication into your Sylius webshop is really a piece of cake 🍰. This extra layer of protection ensures a more secure and reliable experience for both you and your users. For additional information or customization, feel free to check out the sources below.

Sources