skip to content

In Laravel, how does redirect()->intended() send a bookshop customer back to the checkout page after login, and where does url.intended come from?

level: middleimportance: must knowfreq 55%

answer

  1. remember the page, pull it after login
  2. auth middleware -> AuthenticationException
  3. handler calls redirect()->guest($loginUrl)
  4. guest() stores url.intended for GET only
  5. intended($default) pulls the key once

basics

~20 s

When a guest hits a protected page, the exception handler calls redirect()->guest() to the login route, which stores the current URL as url.intended in the session. After login, redirect()->intended($default) pulls that key and redirects there, or to $default.

solid answer

~40 s

The `auth` middleware throws `AuthenticationException` for a guest. For a browser request the exception handler calls `redirect()->guest($loginUrl)`, where the login URL comes from `redirectGuestsTo()`, which defaults to `route('login')`. `guest()` saves the current full URL, query string included, under the session key `url.intended` when the request is a routed GET that does not expect JSON; otherwise it saves the previous URL. After a successful login the controller returns `redirect()->intended(route('account'))`. `intended()` **pulls** `url.intended`, reading and deleting it, so it works once, and falls back to the default you pass, which is `/` if you pass nothing. To set the destination yourself, such as a "Log in to check out" button on the basket page, call `redirect()->setIntendedUrl(route('checkout'))` before redirecting to login.

code

php · 21 lines
php
<?php

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController
{
    public function store(Request $request): RedirectResponse
    {
        $credentials = $request->validate(['email' => 'required|email', 'password' => 'required']);

        if (! Auth::attempt($credentials)) {
            return back()->withErrors(['email' => 'Those details do not match.'])->onlyInput('email');
        }

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

        return redirect()->intended(route('account'));   // back to /checkout if stored
    }
}

go deeper

for a junior

Remember the pair: the auth middleware remembers the page as url.intended, and redirect()->intended($default) sends the user there after login.

for a middle

Explain the chain from Authenticate to AuthenticationException to redirect()->guest(), the GET-only rule, and that intended() pulls the key once.

for a senior

Handle custom flows with setIntendedUrl(), configure redirectGuestsTo() and redirectUsersTo() in bootstrap/app.php, and test expired-session POSTs and JSON clients.

for a principal

Decide how post-login navigation should behave across web, SPA and mobile clients so each gets a consistent redirect or 401 contract.

## The flow in a bookshop A guest fills a basket and clicks **Checkout**, a `GET /checkout?coupon=AUTUMN` route protected by the `auth` middleware. They should see the login form and, once logged in, land back on `/checkout?coupon=AUTUMN`, not on a generic dashboard. Laravel does this with a session key named `url.intended` and two Redirector methods. ## Step by step 1. **The middleware refuses.** `Illuminate\Auth\Middleware\Authenticate` finds no authenticated user on any of its guards and throws `Illuminate\Auth\AuthenticationException`, carrying the URL to send guests to. 2. **Where the login URL comes from.** In Laravel 13 the application builder calls `redirectGuestsTo(fn () => route('login'))` by default. Change it in `bootstrap/app.php` with `->withMiddleware(fn (Middleware $middleware) => $middleware->redirectGuestsTo('/signin'))`. 3. **The handler builds the redirect.** The exception handler's `unauthenticated()` method returns a 401 JSON body for requests that should get JSON. For a browser it returns `redirect()->guest($redirectTo)`. 4. **`guest()` remembers the page.** Before redirecting, `Redirector::guest()` stores a URL under `url.intended`: - the **current full URL** when the request is a GET, has a matched route and does not expect JSON; - otherwise the **previous URL**, because replaying a POST as a GET would be wrong. 5. **The user logs in.** The login action authenticates and regenerates the session; that part is the login flow's business. 6. **`intended()` sends them back.** `redirect()->intended($default = '/')` calls `$session->pull('url.intended', $default)` and redirects there. `pull` reads and removes the key, so the next login starts clean. ## Setting the target yourself Not every trip to the login page starts with the middleware. The basket page might show a **Log in to check out** link: ```php return redirect()->setIntendedUrl(route('checkout'))->route('login'); ``` `setIntendedUrl()` returns the Redirector, so you can chain the redirect. `getIntendedUrl()` reads the value without removing it, which is handy for showing "You'll return to checkout after signing in" on the login page. ## Redirecting from middleware in general The authentication middleware is only one example. Any middleware can end a request early by returning a redirect instead of calling `$next($request)`: - a `EnsureBasketNotEmpty` middleware returns `to_route('basket.show')->with('status', 'Your basket is empty.')`; - the `guest` middleware (`RedirectIfAuthenticated`) sends already-logged-in users away from the login page, to the target set with `redirectUsersTo()` or, by default, a route named `dashboard` or `home` (or a GET route with that path), else `/`. The rule is the same in all cases: return the `RedirectResponse` before `$next()` and the controller never runs. ## Edge cases | Situation | What ends up in `url.intended` | |---|---| | Guest opens `GET /checkout?coupon=AUTUMN` | the full URL with the query string | | Session expired, guest submits `POST /checkout/confirm` | the previous URL, such as the checkout page | | Guest's fetch call expects JSON | nothing: the handler returns 401 JSON instead | | User logs in directly from the home page | nothing, so `intended()` uses its default | ## Testing the flow The behaviour is easy to pin down in a feature test, whichever test runner the project uses: 1. Request `/checkout?coupon=AUTUMN` as a guest and assert a redirect to the login route. 2. Assert the session now holds `url.intended` with the full checkout URL. 3. Post valid credentials to the login route and assert a redirect to `/checkout?coupon=AUTUMN`. 4. Log in once more from the home page and assert the redirect goes to the default instead. The fourth step catches controllers that call `getIntendedUrl()` and forget that only `intended()` removes the key, and the second catches middleware ordering mistakes where the session was not started before the redirect was built. An extra test that posts to a protected route after the session expired shows that the stored URL is the previous page, not the POST endpoint. ## What interviewers want The mechanism in one line (guest stores, intended pulls), the name `url.intended`, the default, and the GET-only rule. Strong candidates mention `setIntendedUrl()` for custom flows and know that redirecting guests is configured with `redirectGuestsTo()` in `bootstrap/app.php` in current Laravel, not in a middleware class you edit.

  • The customer logs in, reaches checkout, logs out, and logs in again from the home page. Where do they land?
    On the default passed to `intended()`. The first `intended()` call pulled `url.intended` out of the session, so the second login finds no stored URL and uses the fallback, `route('account')` in a typical controller or `/` if none is given.
  • How do you send already-logged-in users away from the login page to `/account` in Laravel 13?
    In `bootstrap/app.php`, inside `withMiddleware()`, call `$middleware->redirectUsersTo('/account')`. That configures the `guest` middleware (`RedirectIfAuthenticated`), which otherwise looks for a route named `dashboard` or `home` and then falls back to `/`.
  • Why does `guest()` store the previous URL instead of the current one for a POST?
    After login the user is sent to the stored URL with a GET. Replaying a POST endpoint as a GET would hit a route that may not accept GET or would lose the submitted body, so `guest()` only records the current URL for routed, non-JSON GET requests and otherwise records the page the user came from.

It is like a cloakroom ticket: the doorman (the auth middleware) takes note of where you were heading before sending you to the desk, and the desk hands the note back once you have shown your ID. Use the ticket once and it is gone.

saying these in an interview costs you the question

  • intended() reads url.intended but leaves it in the session for later logins.
  • guest() stores the current URL even when the request is a POST.
  • The login redirect for guests is configured in app/Http/Kernel.php.
  • intended() throws an exception when no intended URL is stored.
  • url.intended is stored in a cookie, not in the session.