User Bundle Login And Security

This guide focuses on the runtime security integration of the bundle.

The login page itself is only one part. A working setup also needs:

  • the provider
  • the firewall
  • public access for the login and recovery routes
  • optional remember me and login throttling
  • optional switch user and impersonation bar

Login Routes

Import the login routes:

YAML

_sfs_login:
    resource: '@SfsUserBundle/config/routing/login.yaml'

PHP

<?php

// config/routes.php
namespace Symfony\Component\Routing\Loader\Configurator;

return static function (RoutingConfigurator $routes): void {
    $routes->import('@SfsUserBundle/config/routing/login.yaml');
};

This exposes:

  • sfs_user_login
  • sfs_user_login_check
  • sfs_user_logout

The check and logout controllers are placeholders. They must be handled by Symfony Security through your firewall configuration.

Provider And Firewall

Use the bundle provider:

YAML

security:
    providers:
        sfs_user:
            id: sfs_user.user_provider

PHP

<?php

// config/routing/login.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', ['providers' => ['sfs_user' => ['id' => 'sfs_user.user_provider']]]);
};

Then connect it to your main firewall:

YAML

security:
    firewalls:
        main:
            pattern: ^/(admin|app)/
            lazy: true
            provider: sfs_user
            form_login:
                provider: sfs_user
                login_path: sfs_user_login
                check_path: sfs_user_login_check
            logout:
                path: sfs_user_logout

PHP

<?php

// config/routing/login.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', [
        'firewalls' => [
            'main' => [
                'pattern' => '^/(admin|app)/',
                'lazy' => true,
                'provider' => 'sfs_user',
                'form_login' => [
                    'provider' => 'sfs_user',
                    'login_path' => 'sfs_user_login',
                    'check_path' => 'sfs_user_login_check',
                ],
                'logout' => ['path' => 'sfs_user_logout'],
            ],
        ],
    ]);
};

Without this, the login form can render but authentication will not happen.

Public Access Rules

The login and recovery pages are public routes.

A practical access control baseline is:

YAML

security:
    access_control:
        - { path: ^/app/login, roles: PUBLIC_ACCESS }
        - { path: ^/app/reset-password, roles: PUBLIC_ACCESS }
        - { path: ^/app/register, roles: PUBLIC_ACCESS }
        - { path: ^/app/invitation, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }

PHP

<?php

// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', [
        'access_control' => [
            ['path' => '^/app/login', 'roles' => 'PUBLIC_ACCESS'],
            ['path' => '^/app/reset-password', 'roles' => 'PUBLIC_ACCESS'],
            ['path' => '^/app/register', 'roles' => 'PUBLIC_ACCESS'],
            ['path' => '^/app/invitation', 'roles' => 'PUBLIC_ACCESS'],
            ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
        ],
    ]);
};

Only keep the public entries for features you really use.

Redirect After Login

The bundle supports a configurable target path parameter:

YAML

sfs_user:
    login:
        target_path_parameter: _target_path

PHP

<?php

// config/packages/sfs_user.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('sfs_user', ['login' => ['target_path_parameter' => '_target_path']]);
};

When this option is enabled, login and register pages preserve that parameter and forward it to the security check path.

Use this when:

  • users are redirected to login from a protected page
  • the application has several post-login landing pages
  • you want custom onboarding flows after authentication

Login Throttling

The bundle does not implement throttling by itself. It relies on the standard Symfony feature.

Example:

YAML

security:
    firewalls:
        main:
            login_throttling:
                max_attempts: 5
                interval: '1 minute'

PHP

<?php

// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', ['firewalls' => ['main' => ['login_throttling' => ['max_attempts' => 5, 'interval' => '1 minute']]]]);
};

Install symfony/rate-limiter if the project does not already include it. Keep this configuration inside the same firewall that handles the login check.

Remember Me

Remember me is also standard Symfony configuration:

YAML

security:
    firewalls:
        main:
            remember_me:
                secret: '%env(APP_SECRET)%'
                lifetime: 2592000
                secure: true
                httponly: true
                always_remember_me: false

PHP

<?php

// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', [
        'firewalls' => [
            'main' => [
                'remember_me' => [
                    'secret' => '%env(APP_SECRET)%',
                    'lifetime' => 2592000,
                    'secure' => true,
                    'httponly' => true,
                    'always_remember_me' => false,
                ],
            ],
        ],
    ]);
};

The example sends the remember-me cookie only over HTTPS. For local HTTP development, configure the cookie policy separately. The login form must submit _remember_me when always_remember_me is false.

For admin-only backoffices, many teams keep it disabled.

Switch User

The bundle supports Symfony switch_user and adds extra authorization checks through SwitchUserVoter.

Base config:

YAML

security:
    firewalls:
        main:
            switch_user:
                role: ROLE_ALLOWED_TO_SWITCH
                parameter: _switch_user

PHP

<?php

// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', [
        'firewalls' => [
            'main' => ['switch_user' => ['role' => 'ROLE_ALLOWED_TO_SWITCH', 'parameter' => '_switch_user']],
        ],
    ]);
};

Typical role setup:

YAML

security:
    role_hierarchy:
        ROLE_ADMIN:
            - ROLE_ALLOWED_TO_SWITCH

PHP

<?php

// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('security', ['role_hierarchy' => ['ROLE_ADMIN' => ['ROLE_ALLOWED_TO_SWITCH']]]);
};

The voter adds extra restrictions:

  • super admins cannot be impersonated
  • admin-to-admin impersonation is restricted

These vetoes depend on the access-decision strategy. Use the unanimous strategy from the installation guide so a role grant does not override a subject-specific denial. The voter recognizes ROLE_ALLOWED_TO_SWITCH; keep that role when using this implementation.

Impersonation Bar

The bundle can inject a visible bar while an admin is impersonating another user.

Example:

YAML

sfs_user:
    impersonate_bar:
        enabled: true
        switch_role: ROLE_ALLOWED_TO_SWITCH
        switch_route: app_entrypoint
        switch_parameter: _switch_user

PHP

<?php

// config/packages/sfs_user.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('sfs_user', [
        'impersonate_bar' => [
            'enabled' => true,
            'switch_role' => 'ROLE_ALLOWED_TO_SWITCH',
            'switch_route' => 'app_entrypoint',
            'switch_parameter' => '_switch_user',
        ],
    ]);
};

This is especially useful in support or customer success tools, because it makes impersonation obvious and gives a quick exit path.

In armonic-standalone, only switch_route is configured and the bar can be enabled when the project needs it.

User Identifiers

The provider resolves the user from your concrete entity class.

The actual identifier depends on the interfaces your entity implements:

  • UserIdentifierEmailInterface for email login
  • UserIdentifierUsernameInterface for username login

Choose one early. Most modern projects use email as the main identifier.

Troubleshooting

  • The login form renders but authentication never happens:
    Check form_login.check_path and provider.
  • Logout throws the placeholder exception:
    Check that logout.path is configured on the firewall.
  • Protected pages keep redirecting to login:
    Check firewall pattern and access_control.
  • Switch user works too broadly:
    Review your role hierarchy and the subject voter behaviour.
  • Install for the base entity, route imports, and first security baseline.
  • Register and reset password to add public onboarding and recovery flows.
  • Admin area if the same firewall must also cover the admin backoffice.
  • OAuth login when standard login is not enough and the project needs external authentication.