Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Enterprise Security Bundle Laravel Package

3brs/enterprise-security-bundle

Standalone Symfony bundle with reusable security primitives: 2FA, passkeys/WebAuthn, magic-link and OAuth login, account lockout, session tracking, IP allow/deny lists, password policy, GDPR account deletion, and runtime settings. Contracts only—wire into your app.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: this is a Symfony bundle, and you are not on Symfony. It requires Symfony 6.4/7.4, PHP 8.3+, registers via config/bundles.php, and leans on scheb/2fa-bundle and Symfony's DI container, security firewalls and event dispatcher. There is no Laravel service provider, no facade, no config-publishing shim, and nothing in the README suggesting one is planned (the only ready-made UI sits on top of Sylius, which is itself Symfony). So a Laravel developer's day-to-day relationship with this package is reference implementation, not dependency. composer require will install, but nothing will wire itself up.

Two ways it is genuinely useful to you:

  1. As a spec/design document. The docs tree under docs/features/ is a well-organised checklist of the 16 security concerns an "enterprise" account system actually has: lockout, password history, passkey ceremonies, GDPR deletion grace periods, IP allow/deny lists, session fingerprints, enumeration-resistant timing. Read that table first, then map each row to what you already have in Laravel (RateLimiter, Password:: rules, signed URLs, Socialite, webauthn-lib). It's a good gap analysis artefact for a security review.
  2. As portable code for the framework-free classes. A handful of primitives have no Symfony coupling worth worrying about: CidrMatcher + the CidrList constraint logic, PasswordSimilarityChecker, UserAgentParser, SessionFingerprintGenerator, DeadlineTimingPadding, GracePeriodCalculator. Those are the ones worth reading line-by-line and, if licensing/attribution suits, lifting. The rest (Abstract*Controller, Abstract*Listener, *Factory wired by DI) does not travel.

If you are dropping it into a Symfony service, order of operations:

composer require 3brs/enterprise-security-bundle
# register SchebTwoFactorBundle + ThreeBRSEnterpriseSecurityBundle in config/bundles.php

Then read docs/configuration.md → entities-and-persistence.md → interface-implementations.md → controllers.md in that order. Do not start at the feature docs; they assume the wiring is done.

Pick the smallest feature first. Password-policy and password-history constraints need no persistence beyond your user table. IP whitelist/blacklist needs one listener and a CIDR list. Get those green before touching passkeys or magic links — those drag in record entities, token cleanup, front-end JS (docs/passkey-frontend.md) and firewall config simultaneously, and you will not be able to tell which layer is broken.

Before you commit to a flow, inventory what the bundle deliberately does not ship: the settings UI, the force-password-change controller, the recovery-codes page, the GDPR cron command, and the session revocation listener. That list (docs/controllers-you-provide.md) is your actual work estimate, and it's larger than the "what the bundle provides" section implies.

Implementation Patterns

Contract-first means your entity layer is the integration surface. The bundle ships interfaces (MagicLinkRecordInterface, SessionRecordInterface, PasskeyCredentialRecordInterface, SocialAccountLinkRecordInterface, CustomerDeletionRequestRecordInterface) plus repository contracts and an UserAnonymizerInterface. You create one Doctrine entity per record, implement the interface, register the repository as a service, and add migrations. Treat this as a design decision you make once — a SecurityRecord supertable with a type column sounds tempting and will fight every repository contract.

User mixins are the second surface. TwoFactorAuth*, Lockable*, PasswordExpiration* traits go on your user entity. The PasswordExpiration* mixin additionally needs getCreatedAt(): ?\DateTimeInterface to serve as the fallback expiration anchor for users who never changed their password — if your user entity doesn't already expose it, wire it, or expiration silently misbehaves for legacy accounts.

Extend the abstract controllers rather than reimplementing flows. Each one hands you the ceremony and expects you to bind the entity, the template, the redirect target and the firewall route names. Keep your subclasses thin: routing + template + redirect. Anything resembling business logic belongs in a service you inject, otherwise you'll have untestable controllers duplicating what the bundle's parent already does.

Use the engines directly via DI in your own services. This is where most of the value is:

  • TotpSecretGenerator / QrCodeGenerator / RecoveryCodeGenerator for 2FA provisioning outside the wizard.
  • TwoFactorEnforcementChecker + TwoFactorMode + TwoFactorAwareAuthenticationSuccessHandler to enforce enrolment at login.
  • MagicLinkTokenGenerator / MagicLinkTokenValidator, always paired with DeadlineTimingPadding on the response path.
  • LockoutPolicy + RateLimitGuard + DynamicRateLimiterFactory for the lockout ladder.
  • OAuthLinkCodeGenerator + CodeChallengeValidator for the confirm-link one-time code (mint + hash, verify with expiry, attempt limit, single use).

Fill the rate limiter's companion map explicitly. RateLimitGuard::$ipCompanionActions ships empty. Without something like ['login' => 'login_ip'], a username-keyed login limit and the lockout counter both miss password spraying by construction: an attacker spreading a handful of common passwords across many accounts never trips a per-account threshold. Give the companion its own rate_limit.{action}.* settings — do not reuse the per-account numbers.

Settings are scoped, not global. FeatureToggle + PolicyFactory + YamlConfigDefaultsProvider give you CUSTOMER / ADMIN / GLOBAL scopes over a runtime store. Pattern: YAML holds defaults/safe values, the store holds overrides, policies read through the factory. Never read the store directly from a controller — you lose the defaulting.

Hook in through the abstract listeners. AbstractIpRestrictionChecker / AbstractIpRestrictionListener (whitelist and blacklist), AbstractPasswordLoginCheckListener (per-scope password sign-in disable). Subclass, register with an appropriate priority, and be deliberate about ordering relative to Symfony's own authentication listener.

For GDPR deletion, cron + anonymizer. GracePeriodCalculator stamps the deadline, AbstractDueDeletionsProcessor is the cron engine, and your UserAnonymizerInterface implementation does the actual scrubbing. The processor should call the anonymizer, not your own deletion logic — otherwise the "due" query and the deletion diverge.

Porting to Laravel: resist building a compatibility layer. The framework-free classes come across; the container-wired factories don't. For the rest, map to native equivalents rather than transliterating Symfony idioms — Laravel's named rate limiters, Password:: defaults, signed temporary URLs, and a maintained WebAuthn library will be better supported than a half-ported PasskeyCeremonyStepManagerFactory. The one idea worth copying wholesale regardless of framework is the confirm-link code pattern: hash at rest, expiry, attempt counter, single use.

Gotchas and Tips

It ships no UI and no concrete controllers. That's the headline. Every "flow" is an abstract base class you extend, every piece of markup is yours, and your route table is yours. Budget accordingly — the bundle removes the ceremony and policy logic, not the integration work.

Persistence is entirely yours. No ORM dependency means no migrations, no indexes, no retention jobs. You need cleanup for expired magic-link tokens, stale session records, consumed deletion requests and revoked passkey credentials. Missing TTL cleanup on magic-link tokens is the classic mistake.

Read CHANGELOG.md before pinning, because the docs have drifted before. Version 2.2.1 exists to correct 2.2.0, which claimed the admin "block account" action "covers both the next sign-in and the ones already open." It doesn't: enabled = false blocks the next sign-in, and revocation only stamps revokedAt — nothing is closed until you implement the session revocation listener. That's a documented guarantee the code does not honour by itself, so wire the listener and verify it with a live test rather than trusting the guide.

Version 2.2.0 closed a real OAuth replay hole. The signed state cookie previously carried no exp; its lifetime rested solely on the cookie's Expires attribute, which an attacker replaying a captured value ignores. Since the link cookie names the account the callback signs in, a copy surviving in a HAR file, proxy log or error report stayed a working sign-in for that account — unaffected by logout, password change or "revoke all sessions." Be on ≥ 2.2.0, and don't assume logout invalidates in-flight OAuth state.

Configure the rate-limiter cache pool first. docs/configuration.md covers it; skip it and you'll get container-time or first-request failures that look unrelated.

Zero stars and zero dependents means you are the QA. 384 unit tests over services and abstract controllers is decent, but it does not exercise your bindings, your firewall ordering, or your Doctrine schema. Plan an integration test per flow on your side (login → 2FA challenge → recovery-code fallback, at minimum) and write them before you start, because the flows are stateful enough that manual testing misses the resumption paths.

Symfony 7.4 is a fresh target. Combined with the explicit symfony/clock requirement added in 2.2.0 (previously only resolved transitively via symfony/security-bundle), pin both Symfony and the bundle to exact versions rather than ^ and upgrade deliberately alongside UPGRADE.md.

Debug with debug:firewall and debug:container. When a flow dead-ends, the failure is almost always one of: wrong firewall name in the scheb_2fa config, a listener registered at the wrong priority, or a controller bound to the wrong route name in docs/routes.md. Check those three before reading bundle source.

Extension points are the abstract classes and the interfaces — treat the concrete services as sealed. Decorate or alias in the container if you must change behaviour; forking a shipped service puts you off the upgrade path in a package with a single maintainer.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings