mpociot/reauthenticate
Laravel package for reauthentication: middleware and traits that require users to re-enter their password before sensitive actions such as changing an email or deleting an account, with a built-in confirmation view and configurable timeout.
Check the version fit before you type composer require. If you're on Laravel 6.2+, stop here: Illuminate\Auth\Middleware\RequirePassword plus the password.confirmation route (Fortify, Breeze, or laravel/ui) is the same GitHub-style sudo UX with zero third-party surface, actively maintained, and tested against your PHP version. This package's last release was 2019-10-08 and it exists to fill a gap that closed a long time ago. Adopt it only if you're pinned to Laravel 5.x and an upgrade is genuinely out of scope, or if you want it as a readable MIT reference to copy from rather than depend on.
Install and publish.
composer require mpociot/reauthenticate
php artisan vendor:publish --provider="Mpociot\Reauthenticate\ReauthenticateServiceProvider"
Pin the version explicitly in composer.json. Verify the provider FQCN against src/ — the publish flag fails silently-ish with a confusing error if the namespace has drifted. Confirm what landed: config/reauthenticate.php, a Blade view, and (probably) a route for the confirmation form.
Read the middleware line by line before writing a single route. src/Reauthenticate.php is short and it is the whole contract. You need three facts out of it:
>= vs >, absolute timestamp vs. duration). Neither is documented.Write those three down in your own words in the PR description. Future-you will not re-derive them.
Register your own alias, not the vendor's, then protect one route.
// app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'sudo' => \App\Http\Middleware\Sudo::class, // your class, see below
];
First use case: the profile/email edit page. It exercises the full loop — authenticated request → redirect to the confirmation form → password POST → redirect back to the originally intended URL — with zero blast radius. Walk it manually once, then protect password change, then API keys, then billing, then account deletion. One route at a time, verifying each.
Smoke-test checklist before you call it done:
php artisan route:list | grep -i reauth — note the route name, URI, and method. The redirect target depends on all three.dd(session()->all()) immediately before and after the confirmation POST. See the key appear.Keep the sensitive-route registry in one reviewable place. Sudo mode earns its keep when a security reviewer can look at a single block and see every protected surface:
Route::middleware(['auth', 'sudo'])->group(function () {
Route::get('/settings/profile', [ProfileController::class, 'edit']);
Route::put('/settings/password', [PasswordController::class, 'update']);
Route::get('/settings/api-keys', [ApiKeyController::class, 'index']);
Route::delete('/account', [AccountController::class, 'destroy']);
});
Twenty scattered ->middleware('sudo') calls means nobody can audit what's protected when the sensitive-action list changes.
Wrap the vendor middleware behind your own class. Call sites reference sudo, never the vendor alias. That makes the eventual migration to password.confirm, a fork, or a homegrown implementation a one-line change in Kernel.php:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Sudo
{
public function handle(Request $request, Closure $next)
{
// Non-HTML clients get a machine-readable signal, not a redirect
// into a Blade form they cannot render.
if ($request->expectsJson() || $request->header('X-Inertia')) {
if (! $this->isReauthenticated($request)) {
return response()->json(
['message' => 'Password confirmation required'],
423 // Locked
);
}
return $next($request);
}
return app(\Mpociot\Reauthenticate\Reauthenticate::class)
->handle($request, $next);
}
private function isReauthenticated(Request $request): bool
{
// Read the key name out of the published config / middleware source.
// Do not guess it.
return $request->session()->has(config('reauthenticate.session_key'));
}
}
Duplicating the guard condition is safer than probing the vendor class with reflection. Keep the duplicated logic to one method and comment it with the source you copied it from.
Handle Livewire, Inertia, and XHR deliberately. The package's failure response is an HTTP redirect to an HTML form. Any of those clients will follow it into a document they can't render, and you'll spend day two debugging "why is my fetch returning HTML." Return 423 Locked from your wrapper and have the frontend open a modal that POSTs to your confirmation route; on success, retry the original request.
Align the timeout with the action, not globally. The package reads one timeout from config. If impersonation should be 5 minutes and billing 60, you have three options: accept one window and document it prominently, put two thin subclasses in app/Http/Middleware/ that set different config values before delegating, or fork. A per-action policy engine is out of scope and always will be — don't pretend otherwise in the ticket.
Write the feature-test matrix before you touch production routes. These tests are both your safety net and your exit characterization suite for the eventual replacement:
// authenticated + confirmed within window -> 200
// authenticated + stale -> 302 to confirmation route
// + intended URL in session
// correct password POST -> 302 to intended URL, key set
// wrong password POST -> back to form, errors, no key
// $this->travel($timeout)->minutes() -> prompts again
// $this->travel($timeout - 1)->minutes() -> passes
Run every case against each guard (web, admin, customer) separately. Every one of these becomes a migration test later, for free.
Log the confirmation somewhere. The package fires no event on success. If you need an audit trail — and for any security narrative you do — override the confirmation controller in app/Http/Controllers/, or observe the session-key transition inside your wrapper and write to your audit log there. Keep the override in app/ so a fork inherits it.
Plan the exit while you install. Preserve the session key's semantics (or translate once, on deploy) so users mid-window aren't bounced to a re-prompt. Keep the sudo alias name stable so no route file changes during the swap. On Laravel 5.x, forking the few hundred lines into app/Http/Middleware/ and deleting the Composer entry is usually cheaper than carrying an unowned dependency — the code is MIT, small, and you had to read all of it anyway.
The maintenance gap is the top risk, not a footnote. Last release 2019-10-08. No CI matrix, no security disclosure process, zero listed dependents. On password-handling code, an unowned dependency is a finding in your next review, not an inconvenience. Either name a team owner and fork it, or don't take it.
Confirm the timeout unit and comparison operator from source, never from docs. Minutes vs. seconds, absolute timestamp vs. duration, >= vs. > at the boundary — none of it is documented and all of it matters. Your timeout / timeout - 1 tests are how you pin it down. If you fork, add a comment at the comparison line.
Multi-guard apps break quietly. The middleware likely resolves the default guard from config. With separate admin and customer guards, verify the password check runs against the guard the request actually authenticated on. A wrong-provider check either always fails (visible, annoying) or compares against the wrong user's hash (an incident). Test each guard individually using the same form.
Fail-open is the failure mode that turns a bug into a breach. A missing config key, an unresolvable guard, a typo'd route group — any of these can pass the request through. Add an assertion test that a protected route redirects an unconfirmed user. If a misconfiguration can quietly skip the check, wrap the middleware so it throws instead.
Passwordless, SSO, and passkey users have nothing to type. OAuth-only users hit a form they can never satisfy. Check $user->password and either skip them with a compensating control (fresh MFA challenge, recent-login assertion) or return a clear "contact your administrator" path. Do not discover this from a bug report from an enterprise SSO customer.
Redirect handling is an open-redirect surface. "Return to intended URL" reads a session key or query param. Validate it's a relative path or an allowlisted host, and test ?redirect=https://evil.example and ?redirect=//evil.example explicitly. Never pipe user input straight into redirect()->intended().
Don't run it alongside password.confirm on the same group. Two middlewares with different session keys, each redirecting to its own form, produces a loop the user can never exit. Migrate group by group, not in place.
PHP 8.1+ is untested upstream. Laravel 6 supported PHP 7.2–8.0, so the package was likely only ever exercised up to 8.0. Expect deprecation notices and possible null-to-non-nullable type errors. Run the full flow on your actual runtime; don't infer safety from composer.json constraints.
Multi-node without session affinity looks like a package bug. Redis or database sessions are fine. File sessions behind a load balancer that doesn't pin the user will reject a confirmation made on node A when the next request lands on node B. Symptom: an inexplicable re-auth prompt immediately after a successful one.
Route name and view collisions. The package registers a route name and publishes a Blade view. If your auth scaffolding publishes a view at the same path or registers the same route name, you get silent shadowing. Re-run php artisan route:list | grep -i reauth after every scaffolding upgrade.
Debugging shortcut: dd(session()->all()) immediately before and after the confirmation POST. Whether the key is absent, present, or present-with-an-unexpected-value tells you in one glance which of the three classic failures you have — middleware not hit at all, confirmation not persisting, or timeout comparison wrong. Almost every flow bug is one of those three.
How can I help you explore Laravel packages today?