3brs / sylius-enterprise-security-plugin
Advanced security plugin for Sylius — 2FA, password policies, account protection, GDPR
Package info
github.com/3BRS/sylius-enterprise-security-plugin
Type:sylius-plugin
pkg:composer/3brs/sylius-enterprise-security-plugin
Requires
- php: ^8.3
- 3brs/enterprise-security-bundle: ~2.3.0
- endroid/qr-code: ^6.0
- league/oauth2-client: ^2.7
- league/oauth2-google: ^4.0
- matomo/device-detector: ^6.5
- patrickbussmann/oauth2-apple: ^0.4
- psr/clock: ^1.0
- scheb/2fa-bundle: ^7.13
- scheb/2fa-totp: ^7.13
- scheb/2fa-trusted-device: ^7.13
- spomky-labs/otphp: ^11.4
- sylius/sylius: ^2.1
- symfony/clock: ^6.4|^7.4
- symfony/rate-limiter: ^6.4|^7.4
- thenetworg/oauth2-azure: ^2.2
- web-auth/webauthn-lib: ^5.0
Requires (Dev)
- behat/behat: ^v3.19.0
- dmore/behat-chrome-extension: ^1.4
- dmore/chrome-mink-driver: ^2.9.3
- friends-of-behat/mink: ^1.11
- friends-of-behat/mink-browserkit-driver: ^v1.6.2
- friends-of-behat/mink-debug-extension: ^v2.1.0
- friends-of-behat/mink-extension: ^v2.7.5
- friends-of-behat/page-object-extension: ^v0.3.2
- friends-of-behat/suite-settings-extension: ^v1.1.0
- friends-of-behat/symfony-extension: ^2.6.2
- friends-of-behat/variadic-extension: ^v1.6.0
- geoip2/geoip2: ^3.0
- nyholm/psr7: ^1.8
- php-http/message-factory: ^1.1.0
- phpstan/phpstan: ^2.1.10
- phpstan/phpstan-doctrine: ^2.0.2
- phpstan/phpstan-strict-rules: ^2.0.4
- phpstan/phpstan-symfony: ^2.0.3
- phpstan/phpstan-webmozart-assert: ^2.0.0
- phpunit/phpunit: ^11.1 || ^12.0.10
- polishsymfonycommunity/symfony-mocker-container: ^v1.0.8
- rector/rector: ^2.0.10
- sylius-labs/coding-standard: ^v4.4.0
- symfony/browser-kit: ^6.4|^7.4
- symfony/debug-bundle: ^6.4|^7.4
- symfony/doctrine-bridge: ^6.4|^7.4
- symfony/dotenv: ^6.4|^7.4
- symfony/framework-bundle: ^6.4|^7.4
- symfony/http-foundation: ^6.4|^7.4
- symfony/http-kernel: ^6.4|^7.4
- symfony/intl: ^6.4|^7.4
- symfony/maker-bundle: ^1.60
- symfony/property-info: ^6.4|^7.4
- symfony/web-profiler-bundle: ^6.4|^7.4
- symplify/easy-coding-standard: ^12.0
Suggests
- geoip2/geoip2: Required when wiring ThreeBRS\EnterpriseSecurityBundle\Session\GeoIp\MaxMindGeoIpLookup as session_management.geoip_service (^3.0)
Provides
None
Conflicts
- api-platform/core: <4.2.0
- api-platform/json-schema: <4.2.0
- api-platform/metadata: <4.2.0
- api-platform/symfony: <4.2.0
- babdev/pagerfanta-bundle: <3.7.0
- doctrine/collections: <1.7.0
- doctrine/data-fixtures: <1.8.0
- doctrine/dbal: <2.13.3
- doctrine/doctrine-bundle: <2.13.1
- doctrine/orm: <2.19.0
- fakerphp/faker: <1.21.0
- friendsofsymfony/rest-bundle: <3.1.0
- jms/serializer-bundle: <4.2.0
- knplabs/knp-menu: <3.3.0
- knplabs/knp-menu-bundle: <3.5.0
- lexik/jwt-authentication-bundle: <2.12
- masterminds/html5: <2.7.5
- payum/core: <1.7.3
- polishsymfonycommunity/symfony-mocker-container: <1.0.6
- sylius/grid-bundle: <1.11.0
- sylius/resource-bundle: <1.13.1
- sylius/sylius: <2.1.0
- sylius/twig-extra: >=0.9.0 <0.9.1
- sylius/twig-hooks: >=0.9.0 <0.9.1
- symfony/css-selector: <4.4.24
- symfony/dom-crawler: <5.4.0
- symfony/framework-bundle: >=5.4.0 <=5.4.20|>=6.0.0 <=6.0.16|>=6.1.0 <=6.1.8|>=6.2.0 <=6.2.2|6.2.8
- symfony/mime: <5.4.0
- symfony/validator: 7.3.0
- symfony/web-link: <5.3.0
- symplify/easy-coding-standard: <10.3.0|12.5.10
- twig/twig: <2.14.7
- webmozart/assert: <1.11.0
- willdurand/negotiation: <3.0
Replaces
None
- dev-main
- dev-docs/manual-test-plan
- dev-feature/adopt-bundle-2.3-abstracts
- dev-SLS-140
- dev-SLS-137
- dev-SLS-125
- dev-SLS-123
- dev-SLS-122
- dev-SLS-121
- dev-SLS-120
- dev-SLS-119
- dev-SLS-114
- dev-SLS-118
- dev-SLS-113
- dev-SLS-112
- dev-SLS-111
- dev-SLS-110
- dev-SLS-109
- dev-SLS-108
- dev-SLS-107
- dev-SLS-106
- dev-SLS-105
- dev-SLS-104
- dev-SLS-103
- dev-SLS-102
- dev-SLS-101
- dev-SLS-100
This package is auto-updated.
Last update: 2026-08-28 15:14:39 UTC
README
Enterprise Security Plugin
3BRS Enterprise Security
- Password Policy
- Password History
- Password Expiration
- Password Change Notifications
- Two-Factor Authentication
- 3rd-party OAuth (Social Login)
- Magic Link Login
- Passkey Login (WebAuthn / FIDO2)
- Account Lockout & Rate Limiting
- Session Management & Login Notifications
- Centralized Security Settings UI
- Self-Service Account Deletion (GDPR)
- Admin IP Whitelist
- Admin IP Blacklist
- Admin Customer Management
- Password Login
Features
Most features ship disabled by default — enable and tune the ones you need (each linked doc covers its options and defaults). Two are active out of the box: Password Login, which is on so existing stores keep working, and Password Policy, which has no on/off switch and applies as soon as the plugin is installed (customers: minimum 8 characters; admins: minimum 12, with an uppercase letter, a lowercase letter, a number and a special character).
| Feature | What it does | Doc |
|---|---|---|
| Password Policy | Configurable password complexity (min/max length + uppercase / lowercase / number / special-character rules), separately per customer and admin group; overrides Sylius's weak 3-character default. | password-policy |
| Password History | Prevents reuse of recent passwords, with a configurable remembered count per group; customer and admin history stored separately. | password-history |
| Password Expiration | Forces a password change after a configurable number of days, with an optional force_change on next login; independent for customers and admins. Existing accounts are measured from their creation date until the first change, so enabling it won't reset everyone at once. |
password-expiration |
| Password Change Notifications | Emails the user on every password change (account settings, forgot-password reset, admin-initiated) with the timestamp, IP address and a secure-account link. | password-change-notifications |
| Two-Factor Authentication | TOTP 2FA (Google Authenticator, Authy, 1Password, …) with QR-code setup, single-use recovery codes, trusted devices and per-group disabled / allowed / enforced modes. Built on scheb/2fa-bundle. |
two-factor-authentication |
| 3rd-party OAuth (Social Login) | Google, Apple and Microsoft sign-in per group, with account-link takeover protection (a single-use, time-limited emailed code), optional domain-gated auto-registration and an extensible provider registry. | oauth-social-login |
| Magic Link Login | Passwordless email sign-in with single-use, hashed, time-limited tokens, anti-enumeration, timing-attack padding and rate limiting. As a passwordless method it bypasses 2FA (the second factor guards password login only). | magic-link-login |
| Passkey Login (WebAuthn / FIDO2) | Passwordless passkeys (Touch ID / Windows Hello / Android lock / hardware keys), multiple labelled keys per user, built on web-auth/webauthn-lib. As a passwordless method it bypasses 2FA (the second factor guards password login only). |
passkey-login |
| Account Lockout & Rate Limiting | Persistent per-user account lockout after N failed sign-ins (auto- or admin-unlock) plus ephemeral request rate limiting (login, password reset and magic link for both groups; registration for customers). | account-lockout-rate-limiting |
| Session Management & Login Notifications | Active-session listing with manual revocation (single or all-other), plus optional email alerts on sign-in from a previously unseen device; pluggable GeoIP lookup. | session-management-login-notifications |
| Centralized Security Settings UI | A single admin page (/admin/security-settings) to configure every feature at runtime — values persist in the database and apply on the next request, no YAML edits or redeploys. |
centralized-security-settings-ui |
| Self-Service Account Deletion (GDPR) | Customer-driven erasure (GDPR right to be forgotten) with a configurable grace period, admin-side cancellation and a cron that anonymizes name / email / phone / address. | account-deletion-gdpr |
| Admin IP Whitelist | Restrict admin-panel access to allowed IPs / CIDR ranges, with a team-wide global list plus optional per-admin lists. | admin-ip-whitelist |
| Admin IP Blacklist | Block specific IPs / CIDR ranges from the admin panel with a global deny-list — always wins over the whitelist, is identity-agnostic, and fails open when empty. | admin-ip-blacklist |
| Admin Customer Management | A Security section on the Sylius customer detail page — force password reset, block / unblock, sign out of all or individual sessions, plus active-sessions and login-history tables. | admin-customer-management |
| Password Login | Global per-group (customer / admin) switch — on by default — for classic email + password sign-in and registration. Turn it off for a group and its login / registration forms are hidden (including the checkout inline sign-in), its forgotten-password pages are closed, the admin panel creates that group's accounts without a password, everyone must use a magic link, passkey or social login, and that group's password expiration / force-change pause too. | password-login |
Installation
This section is for consuming the plugin in your own Sylius project — you register the bundle/plugin and wire the config yourself. If you instead want to work on the plugin itself, skip to Development below: its bundled test application already has the bundle, plugin and routes registered, so you don't repeat these steps.
This plugin requires the standalone
3brs/enterprise-security-bundlepackage and does not work without it. The bundle is the framework-agnostic core (security validators, services, entity contracts); the plugin is the thin Sylius integration layer on top (admin / shop UI, controllers, routes, fixtures). That's why step 1 installs both packages (the bundle comes in as a dependency of the plugin) and step 2 registers them, and the entity traits in step 5 implement interfaces that live in the bundle.
Almost every feature ships disabled by default (see each feature's doc under
docs/). Password Login is on so existing stores keep working, and Password Policy applies from installation with no on/off switch. You enable only what you need in step 3, and the firewall / entity wiring in steps 5–6 is only required for the features you turn on.
-
Require the plugin (its standalone bundle and the Scheb 2FA bundle are pulled in automatically as dependencies):
composer require 3brs/sylius-enterprise-security-plugin
-
Register the bundles in
config/bundles.php(the plugin, its standalone bundle, and the Scheb 2FA bundle it builds on):return [ // ... Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], ThreeBRS\EnterpriseSecurityBundle\ThreeBRSEnterpriseSecurityBundle::class => ['all' => true], ThreeBRS\SyliusEnterpriseSecurityPlugin\ThreeBRSSyliusEnterpriseSecurityPlugin::class => ['all' => true], ];
-
Import the plugin configuration and enable the features you want by creating
config/packages/threebrs_sylius_enterprise_security_plugin.yaml:imports: - { resource: "@ThreeBRSSyliusEnterpriseSecurityPlugin/config/config.yaml" } three_brs_sylius_enterprise_security: # Turn on and tune the features you need — each feature's doc under # docs/ documents its options and defaults (everything is off by default, # except Password Login, which is on).
-
Import the plugin routes by creating
config/routes/three_brs_enterprise_security.yaml(without this none of the plugin endpoints — passkey, magic link, 2FA setup, OAuth, account deletion, settings UI — are registered):three_brs_enterprise_security: resource: "@ThreeBRSSyliusEnterpriseSecurityPlugin/config/routes.yaml"
-
Add the relevant traits to your
ShopUserandAdminUserentities. Include the traits for the features you enabled —TwoFactorAuth*(Two-Factor Authentication),Lockable*(Account Lockout) — andPasswordExpiration*whichever way you configure Password Expiration: Force password reset in admin customer management has no on/off switch of its own, and the button is shown to administrators whether or not the feature is on. (Password Expiration also reads the account creation date viagetCreatedAt()as its fallback for users who have never changed their password — Sylius's baseShopUser/AdminUseralready expose it, so no extra wiring is needed here.) The full set:// src/Entity/User/ShopUser.php use Doctrine\ORM\Mapping as ORM; use Sylius\Component\Core\Model\ShopUser as BaseShopUser; use ThreeBRS\EnterpriseSecurityBundle\Lockout\LockableShopUserInterface; use ThreeBRS\EnterpriseSecurityBundle\PasswordExpiration\PasswordExpirationShopUserInterface; use ThreeBRS\EnterpriseSecurityBundle\TwoFactor\TwoFactorAuthShopUserInterface; use ThreeBRS\SyliusEnterpriseSecurityPlugin\Model\LockableShopUserTrait; use ThreeBRS\SyliusEnterpriseSecurityPlugin\Model\PasswordExpirationShopUserTrait; use ThreeBRS\SyliusEnterpriseSecurityPlugin\Model\TwoFactorAuthShopUserTrait; #[ORM\Entity] #[ORM\Table(name: 'sylius_shop_user')] class ShopUser extends BaseShopUser implements PasswordExpirationShopUserInterface, TwoFactorAuthShopUserInterface, LockableShopUserInterface { use PasswordExpirationShopUserTrait; use TwoFactorAuthShopUserTrait; use LockableShopUserTrait; }
// src/Entity/User/AdminUser.php use Doctrine\ORM\Mapping as ORM; use Sylius\Component\Core\Model\AdminUser as BaseAdminUser; use ThreeBRS\EnterpriseSecurityBundle\Lockout\LockableAdminUserInterface; use ThreeBRS\EnterpriseSecurityBundle\PasswordExpiration\PasswordExpirationAdminUserInterface; use ThreeBRS\EnterpriseSecurityBundle\TwoFactor\TwoFactorAuthAdminUserInterface; use ThreeBRS\SyliusEnterpriseSecurityPlugin\Model\LockableAdminUserTrait; use ThreeBRS\SyliusEnterpriseSecurityPlugin\Model\PasswordExpirationAdminUserTrait; use ThreeBRS\SyliusEnterpriseSecurityPlugin\Model\TwoFactorAuthAdminUserTrait; #[ORM\Entity] #[ORM\Table(name: 'sylius_admin_user')] class AdminUser extends BaseAdminUser implements PasswordExpirationAdminUserInterface, TwoFactorAuthAdminUserInterface, LockableAdminUserInterface { use PasswordExpirationAdminUserTrait; use TwoFactorAuthAdminUserTrait; use LockableAdminUserTrait; }
The traits add mapped columns, so both classes must be Doctrine entities registered as your application's shop and admin user models — which is how a standard Sylius application already ships them. Step 7 creates the columns from that mapping.
Magic link, passkey, OAuth, session management, login notifications and account deletion keep their data in their own tables (foreign-keyed to
ShopUser/AdminUser) and need no traits. -
Configure the firewall for the features you enabled, in
config/packages/security.yaml(andconfig/packages/scheb_2fa.yamlfor 2FA). Each feature's doc underdocs/contains the exact block to copy:- Two-Factor Authentication — the
scheb_2fa.yamlconfig, the shopsuccess_handler, thetwo_factorblocks on theshop/adminfirewalls, and the twoaccess_controlentries grantingIS_AUTHENTICATED_2FA_IN_PROGRESSon the challenge pages. Without those, a token still awaiting its second factor cannot reach the page that asks for it. - 3rd-party OAuth, Magic Link Login, Passkey Login — the
PUBLIC_ACCESSaccess_controlentries that expose their login endpoints.
- Two-Factor Authentication — the
-
Update the database schema to create the plugin tables (
three_brs_*) and the trait columns added in step 5:bin/console doctrine:schema:update --complete --force
The plugin ships no Doctrine migrations. The command above reads the entity mapping and applies it directly, which suits development. For production, generate a migration from the same mapping with
bin/console doctrine:migrations:diff(once the bundle and the step 5 traits are in place) and run it through your usual deployment workflow, likewise for any later upgrade that adds tables or columns. -
Install the bundled assets (e.g. the passkey browser script):
bin/console assets:install
Troubleshooting
Cannot create union with both "object" and class type during cache clear / warmup
If bin/console cache:clear (or any route / API metadata warmup) fails with:
Cannot create union with both "object" and class type.
this is an upstream api-platform regression, not a plugin bug. API Platform's property-metadata scanner (Symfony's PhpStanExtractor → TypeInfo) chokes on generic @template T of object PHPDoc present in some of the plugin's transitive dependencies (e.g. web-auth/webauthn-lib), trying to build an object|SomeClass union that TypeInfo rejects. Because API Platform is enabled by default in Sylius 2, you hit it right after installing the plugin.
It affects api-platform 4.3.x (reproduced on 4.3.5–4.3.7 and still on 4.3.17; no fixed release exists at the time of writing). Until an upstream fix ships, work around it by decorating the property-info extractors with a wrapper that swallows the TypeInfo exception.
Add the decorator class to your application — use the plugin's SafePhpStanExtractor as a reference implementation. It implements every property-info extractor interface (on Symfony 7.3+ also ConstructorArgumentTypeExtractorInterface) and returns null whenever the inner extractor throws Symfony\Component\TypeInfo\Exception\InvalidArgumentException. Then register it over both Symfony's and API Platform's extractor services:
# config/services.yaml services: App\PropertyInfo\SafePhpStanExtractor: arguments: { $inner: '@.inner' } decorates: property_info.phpstan_extractor decoration_on_invalid: ignore app.property_info.safe_php_doc_extractor: class: App\PropertyInfo\SafePhpStanExtractor arguments: { $inner: '@.inner' } decorates: property_info.php_doc_extractor decoration_on_invalid: ignore app.property_info.safe_reflection_extractor: class: App\PropertyInfo\SafePhpStanExtractor arguments: { $inner: '@.inner' } decorates: property_info.reflection_extractor decoration_on_invalid: ignore # API Platform registers its own parallel extractor services — decorate those too: app.property_info.api_platform_safe_phpstan_extractor: class: App\PropertyInfo\SafePhpStanExtractor arguments: { $inner: '@.inner' } decorates: api_platform.property_info.phpstan_extractor decoration_on_invalid: ignore app.property_info.api_platform_safe_php_doc_extractor: class: App\PropertyInfo\SafePhpStanExtractor arguments: { $inner: '@.inner' } decorates: api_platform.property_info.php_doc_extractor decoration_on_invalid: ignore app.property_info.api_platform_safe_reflection_extractor: class: App\PropertyInfo\SafePhpStanExtractor arguments: { $inner: '@.inner' } decorates: api_platform.property_info.reflection_extractor decoration_on_invalid: ignore
The decorator is harmless once the upstream bug is fixed (it only catches an exception that no longer fires), but remove it after you upgrade to a fixed api-platform release to keep your container clean.
Unable to generate an IRI for the item of type ... on API responses
If, after adding the decorator above, an API endpoint that embeds related resources fails with a 400 such as:
Unable to generate an IRI for the item of type "App\Entity\Product\ProductImage"
— for example GET /api/v2/shop/products (embedded product images) or GET /api/v2/admin/product-variants/{code} (embedded channel pricings) — disable API Platform's constructor property-info extractor:
# config/packages/property_info.yaml framework: property_info: with_constructor_extractor: false
On api-platform 4.3.x this extractor runs before Doctrine's and resolves relation properties such as Image::$owner to a bare object (from their @var object|null PHPDoc), hiding the concrete related class that API Platform needs to build nested IRIs like /shop/products/{code}/images/{id}. Turning it off lets the Doctrine extractor resolve the concrete class again. false is also Symfony's current default for this setting, so this only makes it explicit (and silences the related Symfony 7.3 deprecation); the Symfony recipe ships it pre-enabled, which is why you may need to flip it.
Development
This section is only for contributing to / developing the plugin — not for installing it into your own app (that's the Installation section above). The bundled test application under tests/Application/ already has the bundle, plugin, routes and feature config registered (it's part of this repo), so you do not repeat the Installation steps — make init brings the whole stack up ready to go.
Usage
- Develop the plugin in
/src; the framework-agnostic core lives in the separate3brs/enterprise-security-bundlepackage (its own repository) - See
bin/andMakefilefor useful commands
Bootstrapping the dev environment
Spin up the dockerized test application (DB, PHP, schema, assets, fixtures wiring) in one go:
make init
This builds the containers, runs composer install, creates the database, applies the schema with doctrine:schema:update --complete --force and builds the frontend assets. Use make init-tests for the test environment.
In the dev environment the Google, Apple and Microsoft OAuth providers are all swapped for a fake in-memory one, and GeoIP lookup is swapped for a fake that resolves Docker/local addresses so the Active Sessions UI shows a location without a MaxMind database (see tests/Application/config/services_dev.yaml). The social-login buttons therefore work end-to-end without any external credentials. To exercise a real provider flow locally, comment out its FakeOAuthProvider override and fill in your credentials in tests/Application/.env.local.
Testing
After your changes you must ensure that the tests are still passing.
make ci
License
MIT License. See LICENSE for details.
Credits
Developed by 3BRS



