Redirecting unauthorised actions in Laravel

Occasionally I’ve needed to redirect users who are in the wrong place. Maybe they’ve hit the back button after completing a step, or opened up a page from their history that should no longer be accessable.

In your gates and policies, you can push a fallback redirect to the session or append it to the request input, then return it from the exception handler. This means that you can use authorize and can throughout your controllers and routes as normal, without adding boilerplate checks or creating an extra middleware.

Here’s an example of a quiz where the default can middleware prevents applicants from seeing questions before they start and changing their answers after they’ve finished. If they’re in the wrong place, they’ll be sent forward or backward to the intended step.

<?php
Route::middleware('can:access-intro,quiz')
    ->group(function () {
        // Intro pages and enrolment...
    });

Route::middleware('can:attempt-questions,quiz')
    ->group(function () {
        // GET questions and POST answers...
    });

 Route::middleware('can:access-completed-pages,quiz')
    ->group(function () {
        // View score...
    });

In the gate or policy, if unauthorized, push a redirect response to the session before returning `false` as normal.

<?php

/* ... */

class QuizPolicy
{
    use HandlesAuthorization;

    /* ... */

    public function attemptQuestions(User $user, Quiz $quiz)
    {
        // Get user progress
        $progress = $quiz->getProgress($user);

        // Check if not started
        if (empty($progress->stated_at)) {
            // Create a redirect back to the start of the quiz
            $redirect = redirect()->route('quiz.start', ['quiz' => $quiz]);

            // Add redirect response to session temporarily, we'll grab this
            // from the exception handler
            session()->put('canRedirect', $redirect);

            // This action is unauthorized, the "HandlesAuthorization" trait
            // will throw an "AuthorizationException"
            return false;
        }

        // Check if already completed
        if ($progress->completed_at) {
            // Shorter version of above
            session()->put('canRedirect',
                redirect()->route('quiz.completed', ['quiz' => $quiz]));

            return false;
        }

        // This action is authorized \o/
        return true;
    }

    /* ... */

}

Now tweak the render method of the app/Exceptions/Handler.php to grab the redirect from the session. Using pull will remove the redirect from the session so it can’t be reused on another exception. If there was no redirect set, the normal 406 error page will be displayed.

<?php

/* ... */

class Handler extends ExceptionHandler
{
    /* ... */

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        // Check if unauthorized
        if ($exception instanceof AuthorizationException) {
            // Get and delete redirect from session
            $redirect = $request->session()->pull('canRedirect');

            // If exists, return redirect response
            if ($redirect && $request->isMethod('get')) {
                return $redirect;
            }
        }

        return parent::render($request, $exception);
    }
}

All done! This approach probably won’t play nicely with third party packages that provide their own gates, but you may be able to redefine them or intercept the checks.