VerifyCsrfToken.php 4.77 KB
<?php

namespace FootyRoom\Http\Middleware;

use Closure;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Laravel\Lumen\Application;
use Illuminate\Support\InteractsWithTime;
use Symfony\Component\HttpFoundation\Cookie;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Session\TokenMismatchException;
use Symfony\Component\HttpFoundation\Response;

class VerifyCsrfToken
{
    use InteractsWithTime;

    /** @var \Illuminate\Foundation\Application */
    protected $app;

    /** @var \Illuminate\Contracts\Encryption\Encrypter */
    protected $encrypter;

    /**
     * The URIs that should be excluded from CSRF verification.
     */
    protected $except = [];

    /**
     * Create a new middleware instance.
     */
    public function __construct(Application $app, Encrypter $encrypter)
    {
        $this->app = $app;
        $this->encrypter = $encrypter;
    }

    /**
     * Handle an incoming request.
     *
     * @throws \Illuminate\Session\TokenMismatchException
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (
            // Do not verify for guests as Nginx might serve a cached response without a token.
            !$request->user() ||
            $this->isReading($request) ||
            $this->runningUnitTests() ||
            $this->inExceptArray($request) ||
            $this->tokensMatch($request)
        ) {
            $response = $next($request);
            
            if ($this->shouldRefreshToken($request)) {
                $response = $this->addCookieToResponse($request, $response);
            }

            return $response;
        }

        throw new TokenMismatchException();
    }

    /**
     * Determine if the HTTP request uses a ‘read’ verb.
     */
    protected function isReading(Request $request): bool
    {
        return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
    }

    /**
     * Determine if the application is running unit tests.
     */
    protected function runningUnitTests(): bool
    {
        return $this->app->runningInConsole() && $this->app->runningUnitTests();
    }

    /**
     * Determine if the request has a URI that should pass through CSRF verification.
     */
    protected function inExceptArray(Request $request): bool
    {
        foreach ($this->except as $except) {
            if ($except !== '/') {
                $except = trim($except, '/');
            }

            if ($request->fullUrlIs($except) || $request->is($except)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine if the cookie and input CSRF tokens match. This CSRF
     * verification technique is called double-submit cookie. Notice that we are
     * not verifying against token stored in session because our sessions expire
     * every 2 hours which may lead to a lot of token mismatches.
     */
    protected function tokensMatch(Request $request): bool
    {
        $token = $this->getTokenFromRequest($request);
        $cookie = $request->cookie('XSRF-TOKEN');

        return is_string($cookie) &&
               is_string($token) &&
               hash_equals($cookie, $token);
    }

    /**
     * Get the CSRF token from the request.
     */
    protected function getTokenFromRequest(Request $request): ?string
    {
        return $request->input('_csrf_token') ?: $request->header('X-XSRF-TOKEN');
    }

    /**
     * Add the CSRF token to the response cookies.
     */
    protected function addCookieToResponse(Request $request, Response $response): Response
    {
        $config = config('session');

        $response->headers->setCookie(
            new Cookie(
                'XSRF-TOKEN',
                $request->session()->token(),
                $this->availableAt(60 * 60 * 24 * 30), // expire after 1 month
                $config['path'],
                $config['domain'],
                $config['secure'],
                false,
                false,
                $config['same_site'] ?? null
            )
        );

        return $response;
    }

    /**
     * Decide whether new token should be sent to user or not.
     */
    protected function shouldRefreshToken(Request $request): bool
    {
        $cookieToken = $request->cookie('XSRF-TOKEN');

        if (!$cookieToken) {
            return true;
        }

        $sessionToken = $request->session()->token();

        /**
         * Refresh token if session does not have the token anymore (ie. session
         * expired) or tokens are different now. This will ensure that we
         * refresh token every 2 hours, since sessions expire every 2 hours, and
         * also make sure that session has an up to date value.
         */
        if ($sessionToken !== $cookieToken) {
            return true;
        }

        return false;
    }
}