Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
VCL file that describes my JWT implementation in Varnish for Drupal. Part of my DrupalCon Vienna 2017 presentation.
vcl 4.0;
import std;
import var;
import cookie;
import digest;
acl internal {
"192.168.20.0"/24;
}
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 5s;
.first_byte_timeout = 60s;
.between_bytes_timeout = 60s;
}
# Respond to incoming requests.
sub vcl_recv {
#JWT private key
var.set("key",std.fileread("/var/www/html/jwt.key"));
#Sort querystring alphabetically
set req.url = std.querysort(req.url);
#Remove anchors
if(req.url ~ "\#"){
set req.url = regsub(req.url, "\#.*$","");
}
#Remove trailing question mark
if(req.url ~ "\?$"){
set req.url = regsub(req.url, "\?$","");
}
#Announce ESI support
set req.http.Surrogate-Capability="key=ESI/1.0";
#Only cache GET and HEAD
if ((req.method != "GET" && req.method != "HEAD") || req.http.Authorization) {
return (pass);
}
# Do not allow outside access to cron.php or install.php.
if (req.url ~ "^/(cron|install|update)\.php$" && !client.ip ~ internal) {
# Have Varnish throw the error directly.
return (synth(404, "Page not found."));
# Use a custom error page that you've defined in Drupal at the path "404".
# set req.url = "/404";
}
# Handle compression correctly. Different browsers send different
# "Accept-Encoding" headers, even though they mostly all support the same
# compression mechanisms. By consolidating these compression headers into
# a consistent format, we can reduce the size of the cache and get more hits.=
# @see: http:// varnish.projects.linpro.no/wiki/FAQ/Compression
if (req.http.Accept-Encoding) {
if (req.http.Accept-Encoding ~ "gzip") {
# If the browser supports it, we'll use gzip.
set req.http.Accept-Encoding = "gzip";
}
else if (req.http.Accept-Encoding ~ "deflate") {
# Next, try deflate if it is supported.
set req.http.Accept-Encoding = "deflate";
}
else {
# Unknown algorithm. Remove it and send unencoded.
unset req.http.Accept-Encoding;
}
}
# Always cache the following file types for all users.
if (req.url ~ "(?i)\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm|woff)(\?[wd=.-]+)?$") {
unset req.http.Cookie;
return(hash);
}
#Check for cookies
if (req.http.Cookie) {
#Parse cookies
cookie.parse(req.http.cookie);
#Look for session
if(req.http.cookie ~ ".*(SESS[a-z0-9]+)=.*") {
var.set("sessionCookie",regsub(req.http.cookie,".*(SESS[a-z0-9]+)=.*","\1"));
}
#Remove all but some cookies
cookie.filter_except("PHPSESSID,NO_CACHE,ci_session,CI_SESSION,authtoken,jwt_cookie,"+var.get("sessionCookie"));
#Write cookies back to header
set req.http.cookie = cookie.get_string();
#Remove cookie header if empty
if (req.http.Cookie ~ "^\s*$") {
unset req.http.Cookie;
}
}
call jwt;
if(var.get("roles") ~ "administrator") {
std.log("Administrators bypass the cache");
return(pass);
}
#Logged in users cannot access /login and should be redirected to their user profile page
if(req.url == "/login" && req.http.X-login =="true") {
return(synth(302,"/user"));
}
#Exception: cache /user page for anonymous users
if(req.url == "/user" && !cookie.isset("jwt_cookie")) {
return (hash);
}
# Exception: cache password reset form and user registration form
if (req.url == "/user/password" || req.url == "/user/register") {
return(hash);
}
#Cache login redirection page
if(req.url ~ "^/user/login") {
return(hash);
}
# Do not cache these paths.
if (req.url == "/status.php" ||
req.url == "/ooyala/ping" ||
req.url == "/user" ||
req.url ~ "^/admin/.*$" ||
req.url ~ "^/info/.*$" ||
req.url ~ "^/flag/.*$" ||
req.url ~ "^.*/ajax/.*$" ||
req.url ~ "^.*/ahah/.*$" ||
req.url ~ "^/rest/.*$" ||
req.url ~ "^/users?/.*$"
) {
return (pass);
}
#Cache everything else
return(hash);
}
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
unset resp.http.x-url; # Optional
unset resp.http.x-host; # Optional
unset resp.http.Server;
unset resp.http.X-Generator;
unset resp.http.X-Powered-By;
unset resp.http.X-Drupal-Cache;
set resp.http.vary="Accept-Encoding";
}
# Routine used to determine the cache key if storing/retrieving a cached page.
sub vcl_hash {
if (req.http.X-Forwarded-Proto) {
hash_data(req.http.X-Forwarded-Proto);
}
}
sub vcl_backend_response {
#Custom headers to store request information in the response object. Facilitates lurker-friendly bans
set beresp.http.x-url = bereq.url;
set beresp.http.x-host = bereq.http.host;
if (bereq.url ~ "^/admin/content/backup_migrate/export") {
set beresp.do_stream = true;
}
# Don't allow static files to set cookies.
if (bereq.url ~ "(?i)\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm)(\?[wd=.-]+)?$") {
# beresp == Back-end response from the web server.
unset beresp.http.set-cookie;
}
# Allow items to be stale if needed.
set beresp.grace = 6h;
#Perform ESI parsing
if(beresp.http.Surrogate-Control~"ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi=true;
return(deliver);
}
}
sub vcl_synth {
#301 & 302 synths should be actual redirects
if (resp.status == 301 || resp.status == 302) {
set resp.http.location = resp.reason;
set resp.reason = "Moved";
return (deliver);
}
}
sub jwt {
std.log("Ready to perform some JWT magic");
if(cookie.isset("jwt_cookie")) {
#Extract header data from JWT
var.set("token", cookie.get("jwt_cookie"));
var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
var.set("type", regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*?$"},"\1"));
#Don't allow invalid JWT header
if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
#Extract signature & payload data from JWT
var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
var.set("payload", digest.base64url_decode(var.get("rawPayload")));
var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1"));
#Only allow valid userId
if(var.get("userId") ~ "^\d+$") {
#Don't allow expired JWT
if(std.time(var.get("exp"),now) >= now) {
#SessionId should match JTI value from JWT
if(cookie.get(var.get("sessionCookie")) == var.get("jti")) {
#Don't allow invalid JWT signature
if(var.get("signature") == var.get("currentSignature")) {
#The sweet spot
set req.http.X-login="true";
} else {
std.log("JWT: signature doesn't match. Received: " + var.get("signature") + ", expected: " + var.get("currentSignature"));
}
} else {
std.log("JWT: session cookie doesn't match JTI." + var.get("sessionCookie") + ": " + cookie.get(var.get("sessionCookie")) + ", JTI:" + var.get("jti"));
}
} else {
std.log("JWT: token has expired");
}
} else {
std.log("UserId '"+ var.get("userId") +"', is not numeric");
}
} else {
std.log("JWT: type is not JWT or algorithm is not HS256");
}
std.log("JWT processing finished. UserId: " + var.get("userId") + ". X-Login: " + req.http.X-login);
}
#Look for full private content
if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
if(req.http.X-login != "true") {
return(synth(302,"/user/login?destination=" + req.url));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.