March 15, 2022
6 min read time

Two-Minute Tech Tuesdays - Issuing JSON Web Tokens

Two-Minute Tech Tuesday Issuing JWT

In this week’s episode of Two-Minute Tech Tuesday, we'll continue the conversation about JSON Web Tokens. Last week, we demonstrated how JSON Web Tokens are composed and used in Varnish. This week, we'll show you how to turn Varnish into an authentication gateway that issues them.

 

 

Introduction to JSON Web Tokens

JSON Web Tokens are authorization tokens that allow you to securely transport data between two parties. In fact, it's a collection of base64 encoded JSON objects where these objects represent the header, the payload, and the cryptographic signature. The payload is a collection of (public) claims. The signature guarantees the integrity of the payload. 

Because JWT operates within an authorization context, if you want to get access to restricted content, you need to use an Authorization header, the Bearer keyword, and then put in the JSON Web Token. This will get you access to the content. Here's the example from last week's video:

vcl 4.1;

import jwt;

sub vcl_init {
new jwt_reader = jwt.reader();
}

sub vcl_recv {
if (!jwt_reader.parse(regsub(req.http.Authorization,"^Bearer (.+)$","\1"))) {
return (synth(401, "Invalid token"));
}

if(!jwt_reader.set_key("secret") || !jwt_reader.verify("HS256")) {
return (synth(401, "Invalid token"));
}
}

 

This VCL is used to verify the validity of a JSON Web Token. The first thing we do is extract the token from the Authorization header and parse it into the reader object. If that is valid, we'll assign the secret key and mention what algorithm is used to create that signature. If the signature matches, you can carry on. If not, an error is returned.

 

Obtaining a JWT through authentication

Obtaining a JSON Web Token can be done by sending your username and your password to an authentication endpoint. If authentication is successful, you will get the JSON Web Token.

This is usually done by the origin, however, you can offload this from the origin and do it in Varnish with the following VCL:

vcl 4.1;

import jwt;
import json;
import xbody;
import kvstore;
import std;
import crypto;

backend default {
.host="backend.example.com";
.port="80";
}

 

In this VCL example, we import a collection of VMODs. We'll need them for a variety of actions and we'll initialize some of the object contexts in vcl_init.

We'll start off with a JSON Web Token reader object, the corresponding writer object, and a key-value store that will store authentication details in memory that will be loaded from /etc/varnish/auth. These will be usernames and SHA256 password hashes. We'll also initialize another key-value store that will eventually contain all of the secret keys of the JSON Web Tokens.

 

VCL: intercepting the authentication endpoint

If you want to issue JSON Web Tokens, you'll need to intercept the /auth endpoint and offload this from the origin server.

sub vcl_recv {
if(req.url == "/auth" && req.method == "POST") {
std.cache_req_body(1KB);
set req.http.username = regsub(xbody.get_req_body(),"^username=([^&]+)&password=(.+)$","\1");
set req.http.password = regsub(xbody.get_req_body(),"^username=([^&]+)&password=(.+)$","\2");
if(auth.get(req.http.username) != crypto.hex_encode(crypto.hash(sha256,req.http.password))) {
return (synth(401, "Invalid username & password"));
}
return(synth(700));
}
if (!jwt_reader.parse(regsub(req.http.Authorization,"^Bearer (.+)$","\1"))) {
return (synth(401, "Invalid token"));
}

if(!jwt_reader.set_key(keys.get(jwt_reader.to_string())) || !jwt_reader.verify("HS256")) {
return (synth(401, "Invalid token"));
}
}

 

When doing this, we want to extract the username and the password from the request body and store them in the corresponding headers. The username can be used to lookup the password hash in the authentication key-value store. If that doesn't match a SHA256 hash of the password we retrieved, then we have to return a 401 with an Invalid username & password message.

If it was successful, then we can issue the JSON Web Token by returning a custom 700 synthetic response.

 

VCL: preparing the synthetic response

This causes a transition to the vcl_synth subroutine where we intercept the 700 status, reassign a 200 OK, and call a custom create_jwt subroutine. This will set the resp.http.jwt header that we'll use to extract the JSON Web Token and parse it into the response body.

sub vcl_synth {
set resp.http.Content-Type = "application/json";
if(resp.status == 700) {
set resp.status = 200;
set resp.reason = "OK";
call create_jwt;
set resp.body = "{" + {""token": ""} + resp.http.jwt + {"""} + "}";
} else {
set resp.body = json.stringify(resp.reason);
}
unset resp.http.jwt;
return(deliver);
}

 

VCL: creating the JWT

Let's dive a bit deeper and look at the create_jwt subroutine. This is a custom subroutine that will use the jwt_writer context to set the algorithm, the type — which will be part of the header — and a collection of claims.

sub create_jwt {
jwt_writer.set_alg("HS256");
jwt_writer.set_typ("JWT");
jwt_writer.set_sub(req.http.username);
jwt_writer.set_iss(req.http.host);
jwt_writer.set_iat(now);
jwt_writer.set_duration(2h);
set resp.http.key = crypto.uuid_v4();
set resp.http.jwt = jwt_writer.generate(resp.http.key);
keys.set(resp.http.jwt,resp.http.key);
unset resp.http.key;
}

The subject is the username that we extracted. The issuer is the host header. The issued at is set to now. And the duration is set to two hours, which will set the not valid before and the expiration time.

 

We'll also need the secret key, which will be randomly generated and stored into resp.http.key. We'll use that key as a value in jwt_writer.generate() to generate the full JWT. This will be stored in resp.http.jwt for later use. These values are also stored in the keys key-value store, which we'll use for validation later on.

 

That was a very lengthy, yet insanely powerful VCL example. We hope you can appreciate what this can mean for your application. It allows you to offload your entire authentication JSON Web Token issuing and verification logic from the origin, to the "edge" in Varnish. This will take away stress from the origin application and be able to cache pages that otherwise wouldn't be cacheable.

Join us next week for another Two-Minute Tuesday topic, presented to you in 2 minutes or less!

 

varnish_6_by_example_download