jeremykenedy/laravel2step
Laravel2step adds two-step verification to Laravel apps, providing middleware, routes, views, config, and database support for requiring an extra verification step after login. It helps secure user accounts with minimal setup and a customizable flow.
Architecture fit. laravel2step is a thin, opinionated second-factor layer: a middleware, a controller, a notification, a model trait, a migration, and a publishable config/views set. It slots into the standard Laravel HTTP kernel and auth flow rather than introducing its own guard or service provider lifecycle, so it composes cleanly with existing apps. v6.0.0 materially improves this fit by removing the dependency on the host application's App\Http\Controllers\Controller — the package no longer breaks when the skeleton's base controller changes shape, which was the root cause of it being unusable on Laravel 11+. That is the single most important structural change in this release: the package is now decoupled from application scaffolding conventions it never controlled.
Integration feasibility. High for greenfield or recently-upgraded apps on Laravel 12/13 and PHP 8.2+. The upgrade is a single composer require jeremykenedy/laravel2step:^6.0 because nothing on ^5.0 or below resolves to it automatically — the upgrade is opt-in and cannot be pulled in transitively, which is good for change control and bad for patching cadence (see Operational Impact). There is no reported schema migration for the hasOne → belongsTo fix, but that correction implies the relationship now reads userId on the two-step table; the actual column name and whether existing installs already match it needs verification before we promise a no-migration upgrade. Two integration surfaces are frozen at publish time: config and views. Published copies do not receive any of this release's fixes, so any app that has published either inherits the code changes but not the CSP, CDN, jQuery, or sender fixes.
Technical risk. Moderate, and concentrated in behaviour rather than compatibility:
$next($request) before checking verification, meaning protected controller actions executed in full for unverified users and the response was discarded. This release fixes the ordering. The risk to us is retrospective, not forward-looking: any side effects those actions performed — writes, audit entries, outgoing mail, third-party API calls, rate-limit consumption — were happening pre-verification in production. We should treat this as a potential past exposure and ask whether any protected route had state-changing side effects.TwoStepController's base class changed to Illuminate\Routing\Controller. Any app that extended it and relied on inherited $this->authorize() or $this->validate() will fatal. This is a narrow but hard failure, and it will not surface in CI unless CI exercises the verification routes.maxcdn.bootstrapcdn.com (3.3.7) to cdnjs.cloudflare.com (3.4.1). A Content Security Policy whitelisting only the old host will render the verification page unstyled. Markup-compatible, so cosmetic only — the flow still works, which is the right failure mode for a security interstitial.mysql connection now falls back to the app default. Multi-connection apps whose two-step table lives on mysql need LARAVEL_2STEP_DATABASE_CONNECTION=mysql set explicitly or they get table-not-found errors at the worst possible moment.Key questions.
TwoStepController, and did any protected route perform writes, logging, notifications, or outbound calls before the verification redirect?maxcdn.bootstrapcdn.com, and is the two-step table on a non-default DB connection anywhere?MAIL_FROM_ADDRESS?Stack fit. Best fit is Laravel 12 or 13 on PHP 8.2–8.5 — the only combination with real CI coverage. Laravel 11 is a judgement call: the release notes explicitly say to test it yourself or stay on v5. Laravel 10 and below is effectively unsupported despite the permissive constraint, because those framework versions cannot be installed cleanly enough to test against. Treat the composer constraint as a compatibility floor, not a support statement.
Migration path. Because 6.0.0 is opt-in and does not auto-resolve, the safe pattern is: pin ^5.0 on the current branch, cut an upgrade branch, composer require jeremykenedy/laravel2step:^6.0, then run the regression checklist below before merging. No data migration is described, but confirm the relationship column change against the existing schema on a restored production-shaped database rather than a fresh one.
Compatibility checklist before merge:
extends TwoStepController and any use of authorize()/validate() inside it.config/laravel2step.php and resources/views/vendor/laravel2step against 6.0 defaults; decide per-file whether to re-publish, hand-merge, or consciously accept the old version. The jQuery bump and CDN change both only reach us through this step.style-src allows cdnjs.cloudflare.com, or pin LARAVEL_2STEP_BOOTSTRAP_CSS_CDN to whatever the policy already allows.LARAVEL_2STEP_DATABASE_CONNECTION is either correct or unnecessary.LARAVEL_2STEP_EMAIL_FROM is set. If not, the package now defers to application mail config — verify the resulting sender passes SPF/DKIM for the domain we send from.LARAVEL_2STEP_EMAIL_SUBJECT is documented as removed; it was never a real key, so if it appears in our .env files it was always dead and can be deleted.Sequencing. (1) Inventory consuming apps and their Laravel/PHP versions. (2) Audit for TwoStepController subclasses and pre-verification side effects — this is the part with retrospective risk. (3) Reconcile published config and views. (4) Upgrade the lowest-risk app first, on a branch, with an end-to-end manual pass of enrol → verify → session → logout. (5) Explicitly test that an unverified user hitting a protected route is now blocked and that the blocked action left no trace. (6) Roll out app by app. Do not batch this with other dependency work; the middleware ordering change deserves its own blast radius.
Maintenance. This is the release that makes the package maintainable rather than merely working: 81 Pest tests on in-memory SQLite, GitHub Actions replacing a dead Travis config, Dependabot scoped to actions, a working Scrutinizer config, Pint, composer audit, and a weekly scheduled run so a new Laravel release surfaces as a red build rather than a production incident. For a TPM that weekly cron is the most valuable line in the changelog — it converts "will this break silently?" into a monitored signal. The counterweight is bus factor: the release is three PRs from a single maintainer, and the project's health depends on that individual's continued attention. Worth a periodic check that the scheduled runs are still green, not just that the package is installed.
Support. Genuinely bifurcated. Laravel 12/13 is tested and supported. Laravel 11 and below is explicitly "test it yourself or stay on v5," which is honest but means a meaningful share of the installed base has no supported upgrade path to the security fixes in this release — most notably the middleware ordering fix. If we have apps on 11 or below, the correct move is to schedule the framework upgrade first and take the package upgrade second, not to run v5 indefinitely.
Scaling. The per-request cost is a middleware check plus a relationship load against the two-step table. Fine for typical authenticated-app volumes; the release gives no indication of caching, and none was promised. The test suite running on in-memory SQLite means query behaviour under production MySQL/Postgres — indexes, connection handling, the new default-connection fallback — is not exercised by CI. Low risk given the query is a simple keyed lookup, but it is untested rather than proven.
Failure modes and their blast radius:
TwoStepController subclass — hard failure confined to apps that extended it; surfaces immediately on deploy, so it fails loudly rather than subtly.TypeError crash is fixed, but the new fallback to app mail config can produce a sender that fails SPF/DKIM, so verification emails land in spam. This changes a loud failure into a quiet one, which is the harder operational mode.Ramp-up. Low. One config file, one trait, one middleware registration, a documented env surface, and .env examples that now accurately describe which values are optional. The cognitive load is not in learning the package; it is in the frozen-asset problem — understanding that publishing config or views opts you out of future fixes. That needs to be stated in our internal runbook, because it is not obvious from the README and it silently applies to every future release, not just this one.
How can I help you explore Laravel packages today?