Cookies from the Dark Side

I spent some hour trying to get Laravel and Varnish live together peacefully, and mixing various sources from the web I obtained some result.

The base issue is that Laravel drops a session cookie for every users, even when not authenticated, and this fools any evaluation about pass/hash contents in Varnish. The secondary issue (more related to my own deployment) is that authentication happens via OAuth through another instance, so I cannot just relay on pass'ing POST login requests and replying a doors-opening cookie.

Mashing up many ideas and snippets, I ended with the following Varnish configuration (here integrally dumped, even parts not related, for my own future reference):

backend default {  
  .host = "127.0.0.1";
  .port = "8080";
}

sub vcl_recv {  
  if (req.restarts == 0) {
    if (req.http.x-forwarded-for) {
      set req.http.X-Forwarded-For =
        req.http.X-Forwarded-For + ", " + client.ip;
    } else {
      set req.http.X-Forwarded-For = client.ip;
    }
  }
  if (req.method != "GET" &&
    req.method != "HEAD" &&
    req.method != "PUT" &&
    req.method != "POST" &&
    req.method != "TRACE" &&
    req.method != "OPTIONS" &&
    req.method != "DELETE") {
      return (pipe);
  }

  if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
  }

  if (req.url ~ "\.(png|gif|jpg|css|js|ico|woff|woff2|svg)") {
    unset req.http.cookie;
    return (hash); 
  }

  if (req.http.Cookie) {
    /*
      Ignore Google Analytics incoming cookies
    */
    set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_[_a-z]+|has_js)=[^;]*", "");
    set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");

    if (req.http.Cookie == "") {
      unset req.http.Cookie;
    }
  }

  if (req.http.Cookie) {
    return (pass);
  }

  return (hash);
}

sub vcl_backend_response {  
  /*
    If backend says "yeah", strips all outgoing cookies
  */
  if (beresp.http.X-No-Session ~ "yeah" && bereq.method != "POST") {
    unset beresp.http.set-cookie;
  }

  /*
    Aggressive assets caching
  */
  if (bereq.method == "GET" && bereq.url ~ "\.(png|gif|jpg|css|js|ico|woff|woff2|svg)$") {
    unset beresp.http.set-cookie;
    set beresp.ttl = 5d;
    set beresp.http.cache-control = "max-age = 3600";
  }
  else {
    set beresp.ttl = 300s;
  }

  return (deliver);
}

sub vcl_deliver {  
  if (obj.hits <= 0) {
    set resp.http.X-Cache = "MISS";
  }

  set resp.http.Cache-Control = "max-age = 2678406";
}

Everything revolves around the "yeah" header, inspired by this post on StackOverflow: everytime it is present on a backend response, all outgoing cookies are suppressed and the session is not activated, so to raise the hits from the cache.

On Laravel side, I've added the following Middleware:

<?php

namespace App\Http\Middleware;

use Closure;  
use Session;

class StripSessionsIfNotAuthenticated  
{
  public function handle($request, Closure $next)
  {
    if(auth()->check() || strstr($request->route()->getPath(), 'oauth/validate')) {
      return $next($request);
    }
    else {
      $response = $next($request);
      $response->headers->set('X-No-Session', 'yeah');
      return $response;
    }
  }
}

Here, the X-No-Session = yeah header is always pushed but when the user is already logged in or (more important) when I'm bouncing to the application at the end of an OAuth handshake (in my case, on the /oauth/validate endpoint). Which means: when the user is logging in, and he deserves a cookie to authenticate him and bypass the Varnish's cache.

Final step: once the user logs out, I have to completely destroy the client-side cookie to back in the "hard hits from cache" state. So, in AuthController I've added:

public function getLogout()  
{
  Auth::guard($this->getGuard())->logout();
  Session::flush();
  Cookie::queue(Cookie::forget('laravel_session'));
  return redirect(env('TOOL_URL') . '/auth/logout');
}

And the magic happens...