marrow / warden
Account security scaffolding for Marrow — login, registration, password reset, email verification, remember-me, and a 2FA login challenge. Publishes editable controllers/views/form-builder Forms (built on marrow/ui's components) into your app (Breeze-style) on top of a small, versioned runtime (toke
Requires
- php: >=8.2
- marrow/form-builder: ^1.0
- marrow/framework: ^2.2
- marrow/ui: ^1.0
Requires (Dev)
- pestphp/pest: ^3.8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-02 10:06:18 UTC
README
Account security scaffolding for Marrow — login, registration, password reset, email verification, "remember me", and a 2FA login challenge.
Breeze-style, not Fortify-style: php forge warden:install publishes real, plain, fully-editable
controllers/views/routes/migrations into modules/Auth/ — nothing is hidden behind the package at runtime
for the web-facing flow. Only the parts that are awkward (and risky) to hand-roll stay in the package as an
ordinary versioned dependency:
PasswordResetBroker— hashed, single-use, expiring password-reset tokens.EmailVerifier— stateless, HMAC-signed email-verification links (no extra table).RememberMeBroker— persistent "remember me" login cookie.Mail\ResetPasswordMail/Mail\VerifyEmailMail— the two transactional emails.
The published views are themselves built entirely out of marrow/ui's
field/button/checkbox/alert/form components (a real require, not copied in) — restyle every login/
register/reset form at once by overriding those components (ComponentRegistry::registerAs(...)), rather than
editing each published Twig file individually. The generated views still work exactly the same if you'd
rather hand-edit them directly — they're yours either way.
Validation itself is marrow/form-builder (also a real
require): each controller validates through a declarative Form class published alongside it
(Forms/LoginForm.php, etc.), and the view renders it with a single
{{ component('form', {form: form, ...}) }} call via marrow/ui's form-builder bridge — no
{% for %}-over-fields, no separate old()/has_error() wiring to keep in sync with the controller's
validation rules. A failed $form->isValid() re-renders the same view directly with that $form (carrying
its own submitted values and errors) rather than redirecting back with flashed session data — see
LoginController::login()'s docblock for why every controller action that can return either a redirect or
a rendered view is typed Symfony\Component\HttpFoundation\Response, not Marrow\Http\Response
(RedirectResponse isn't a subtype of it).
Why this exists
marrow/framework's Marrow\Auth\* is a solid, low-level authentication/authorization toolkit (Argon2id
hashing, session + JWT guards, Gate/Policy, RBAC, TOTP 2FA) — but it deliberately ships no actual
login/registration flow. The skeleton's own Modules\Account\AccountModule says so explicitly: it owns the
User model and RBAC/2FA/audit schema, and nothing else, on purpose.
That leaves real gaps every app ends up solving itself, inconsistently:
users.email_verified_atexists in the base migration, but nothing ever sets it.- There is no password-reset flow at all.
- There is no
remember_tokencolumn or persistent-login cookie. HasTwoFactorprovides the TOTP primitives, but nothing callsverifyTwoFactor()at login — a user who enables 2FA is not actually protected by it unless something bridges the gap. Warden'sTwoFactorChallengeControlleris exactly that bridge.
Install
composer require marrow/warden php forge warden:install php forge migrate
Re-running warden:install is safe — existing files are skipped unless you pass --force.
The command prints the manual follow-ups it can't safely automate: adding the remember/verified
middleware aliases to config/middleware.php, and making sure MAIL_DSN/APP_KEY are configured (the
latter signs verification links and is required anyway for HasTwoFactor's secret encryption).
What gets published
modules/Auth/
├── AuthModule.php
├── routes.php
├── Controllers/
│ ├── LoginController.php
│ ├── RegisterController.php
│ ├── ForgotPasswordController.php
│ ├── ResetPasswordController.php
│ ├── VerifyEmailController.php
│ └── TwoFactorChallengeController.php
├── Middleware/
│ ├── EnsureEmailIsVerified.php
│ └── AttemptRememberLogin.php
├── Forms/
│ ├── LoginForm.php
│ ├── RegisterForm.php
│ ├── ForgotPasswordForm.php
│ ├── ResetPasswordForm.php
│ └── TwoFactorChallengeForm.php
├── Views/
│ └── *.html.twig
└── Database/Migrations/
├── ..._create_password_reset_tokens_table.php
└── ..._add_remember_token_to_users_table.php
All of it is yours from that point on — rename routes, restyle views, add fields to registration, whatever
the app needs. The package itself never reaches into modules/Auth/ again.
Requirements
marrow/framework^2.2marrow/ui^1.0 andmarrow/form-builder^1.0 (both realrequired dependencies, pulled in automatically)- A mailer configured (
config/mail.php) for password reset / verification emails to actually send. APP_KEYset (php forge key:generate) — used to sign email-verification links.
License
MIT — see LICENSE.