skip to content

In Laravel, how does the password.confirm middleware work, and which routes must a hand-built app define for it?

level: middleimportance: should knowfreq 35%

answer

  1. RequirePassword behind the alias
  2. auth.password_confirmed_at timestamp
  3. password_timeout: 10800 seconds
  4. redirects to route named password.confirm
  5. JSON requests get a 423

basics

~20 s

The password.confirm middleware (RequirePassword) lets a request through only if the session's auth.password_confirmed_at is within auth.password_timeout, three hours by default. Otherwise it redirects to the route named password.confirm, whose POST handler checks the password and calls $request->session()->passwordConfirmed().

solid answer

~40 s

`password.confirm` is an alias for `Illuminate\Auth\Middleware\RequirePassword`. It reads `auth.password_confirmed_at` from the session and compares it with `auth.password_timeout`, which the skeleton sets to `env('AUTH_PASSWORD_TIMEOUT', 10800)` seconds. If the confirmation is older, a JSON request gets a 423 response with "Password confirmation required.", and any other request is redirected with `redirectGuest()` to the route named `password.confirm`, which also stores the intended URL. A hand-built app defines two routes: a `GET` named `password.confirm` that shows a form with a `password` field, and a `POST` that checks the password with `Hash::check`, calls `$request->session()->passwordConfirmed()` to stamp the time, and returns `redirect()->intended()`. Both sit behind `auth`, and the POST should be throttled. Middleware parameters can override the route and timeout per use, as in `password.confirm:password.confirm,300`.

code

php · 21 lines
php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;

Route::get('/confirm-password', fn () => view('auth.confirm-password'))
    ->middleware('auth')->name('password.confirm');

Route::post('/confirm-password', function (Request $request) {
    if (! Hash::check($request->password, $request->user()->password)) {
        return back()->withErrors(['password' => 'The provided password does not match our records.']);
    }

    $request->session()->passwordConfirmed();

    return redirect()->intended();
})->middleware(['auth', 'throttle:6,1']);

Route::get('/staff/kiosk-settings', fn () => view('staff.kiosk-settings'))
    ->middleware(['auth', 'password.confirm']);

go deeper

for a junior

Know that password.confirm makes signed-in users re-enter their password before a sensitive page, and that it remembers the confirmation for a while.

for a middle

Explain the auth.password_confirmed_at session timestamp, the 10800-second default, the 423 JSON response, and the two routes you must define.

for a senior

Apply it deliberately: stricter per-route timeouts, remember-me users, throttling the POST, and SPA handling of 423.

for a principal

Define which actions require fresh proof across products, and align password confirmation with passkeys and two-factor step-up.

## What password confirmation is for Some actions deserve proof that the person at the keyboard still knows the password, even though they are signed in: changing the email address, downloading personal data, or, on a kiosk app's staff screen, changing which terminal settings members can see. Laravel ships a middleware for this; how long a confirmation should last is a policy question, and Laravel only provides the knob. ## The middleware `password.confirm` is one of the framework's built-in middleware aliases and points at `Illuminate\Auth\Middleware\RequirePassword`. On each request it: 1. reads `auth.password_confirmed_at` from the session (0 if missing); 2. compares `now - confirmed_at` with the timeout: the middleware parameter if given, otherwise `auth.password_timeout`; 3. if the confirmation is still fresh, passes the request on; 4. if not, and the request expects JSON, returns **HTTP 423** with `{"message": "Password confirmation required."}`; 5. otherwise returns `redirect()->guest(route('password.confirm'))`, which also saves the current URL as the **intended** URL. | Setting | Default | Where | |---|---|---| | Timeout | 10800 seconds (3 hours) | `password_timeout` in `config/auth.php`, `AUTH_PASSWORD_TIMEOUT` | | Confirmation route | named `password.confirm` | first middleware parameter | | Per-route timeout | none | second middleware parameter, in seconds | `RequirePassword::using('password.confirm', 300)` builds the string `...RequirePassword:password.confirm,300` for you. ## The two routes you write Without a starter kit or Fortify, nothing defines the confirmation screen for you: - **`GET /confirm-password`**, named `password.confirm`, behind `auth`, returning a form with a single `password` field. - **`POST /confirm-password`**, behind `auth` and a throttle, which: 1. checks `Hash::check($request->password, $request->user()->password)`; 2. on failure, returns back with an error on `password`; 3. on success, calls `$request->session()->passwordConfirmed()`, which stores `Date::now()->unix()` under `auth.password_confirmed_at`; 4. returns `redirect()->intended()` to resume the original action. If the `GET` route is missing or named differently, the middleware's redirect fails because `route('password.confirm')` cannot be generated. ## Protecting routes Attach the alias to each sensitive route, for both the form and its submission: - `Route::get('/staff/settings', ...)->middleware(['auth', 'password.confirm'])` - `Route::post('/staff/settings', ...)->middleware(['auth', 'password.confirm'])` The confirmation lives in the **session**, so `invalidate()` at logout removes it, and signing in never sets it. ## Interaction with other features - A user restored by **remember me** has not typed a password in this session; password confirmation is a natural gate for them. - Fortify registers these routes itself, and its two-factor and passkey management routes use the same middleware. - An SPA should treat 423 as "show the confirm dialog", post to the confirmation endpoint, then retry. ## Common mistakes - **Protecting only the form.** If the `GET` page is behind `password.confirm` but the `POST` that saves the change is not, a scripted request skips the prompt. - **Forgetting throttling on the confirmation POST.** It is a password-guessing endpoint for anyone holding a signed-in session, which is why the documented example adds `throttle:6,1`. - **Storing the confirmation yourself.** Writing a custom session key means `RequirePassword` never sees it; call `passwordConfirmed()` so the middleware and the timeout agree. - **Assuming a fresh login counts as confirmation.** Signing in does not set `auth.password_confirmed_at`, so a user who just logged in is still prompted by `password.confirm`.

  • What does an SPA receive from a password.confirm-protected endpoint when confirmation is stale?
    If the request expects JSON, `RequirePassword` returns HTTP 423 with the message "Password confirmation required." instead of redirecting. The SPA should show a confirm-password dialog, post the password to the confirmation endpoint, and retry the original call.
  • How do you require a fresher confirmation on one especially sensitive route?
    Pass the timeout as the second middleware parameter, for example `password.confirm:password.confirm,300` for five minutes, or build it with `RequirePassword::using('password.confirm', 300)`. Other routes keep using `auth.password_timeout`.

saying these in an interview costs you the question

  • password.confirm asks for the password on every request to the route.
  • The confirmation timestamp is stored on the users table.
  • Laravel defines the password.confirm view route automatically without a kit.
  • The default confirmation window is 30 minutes.
  • A JSON request is redirected to the confirmation page like a browser.