/ Tech

Yes, you can invalidate JSON Web Tokens (JWT).

JSON Web Tokens are great: they have a well-defined schema, and are simple to implement both as a provider and an integrator. This simplicity has side effects though, and often leads to limitations. For instance; can you invalidate/revoke a token, or "log a user out"?

Client side logout - i.e "ditch the token" - is not a solution; rather than actually invalidating the token, this simply loses it - often via purging it from local storage. This is a common practice when developing Single Page Applications (SPA) with tools like React, Vue, or Angular.. but it's not ideal.

Although your application has lost the token: the token is still valid, and were an attacker to gain access to it, they would be able to utilise it (in full) until the expiry time set on it; and if they've gained access to the token shortly after it was generated, this could be a worryingly long time!

Another mechanism often used is that of "short validity durations" - i.e having a token expire after a small duration such as 2 hours. Whilst it's a step forward, and reduces the time period that a leaked token could be abused - it's still worth highlighting the potential damage an attacker could do in even half an hour.

From a security point of view, we need a way of actually revoking tokens; we need the ability to proactively invalidate tokens, being able to minimise the damage should a token fall in to malicious hands.

Here lies the problem though: logging out requires intervention on behalf of the server, and if we need the server to act on our token - then we need the server to be able to determine (or remember) which tokens are active. This naturally introduces state in to our API, something that most APIs actively avoid.

Whilst removing state is an admirable aim from an architectural point of view, and has many merits, it's not ideal from a security angle. The inability to specify a per-action unique identifier (or "nonce"), proposes a few problems - including toking revocation, a lack of "security in depth", and replay attacks to name but three.

Stateless architectures can cause quite a few security headaches. But that doesn't mean you can't log out, or invalidate tokens, JSON Web Tokens (JWT).

JWT is the token format, and nothing else.

It's a common misconception that JWT itself is the barrier to token revocation and invalidation, but this is an obfuscation of what JWT actually is: a token format, and a handful of recommendations regarding token communication. It has nothing to do with application state - thats your own architectural decision, and whilst it may be a good one - it's also the real security obstacle.

There's no denying that working with JWT is a breeze from a development point of view: simply POST off a { username, password } pair via HTTPS, and recieve back a 200 response complete with token, and send that token with any subsequent requests. Easy.

As a provider (i.e API server) your job is to make things easy for an integrator (i.e API consumer), but it's also to ensure the security of the service you're providing to the user. That of course means you need to keep their data confidential - via authenticating the user, and providing strict access control measures. Revoking access - in addition to allowing a user to "de-authenticate" - clearly come within the remit of providing adequate security controls.

State itself doesn't mean cookies, nor does it mean packing the session with data; it doesn't change the way your API fundamentally works, nor does it provide enforce any additional steps for the consumer of your API. It does however, enable better security practices.

Minimal State, a HOWTO

If you were to write a minimalistic JWT implementation - not that you should - the token generation would look something like this:

var generateJWT => (payload) {
    const secret = "server_side_secret";
    
    const header = base64UrlEncode({
        "alg" : "HS256",
        "typ" : "JWT"
    });
    
    payload = base64UrlEncode(payload)
    
    const signature = HMACSHA256( header + "." + payload + "." + secret );
    
    return header + "." + payload + "." + signature;
}
    
generateJWT({
    "sub": "1234567890",
    "name": "Fergus In London",
    "iat": 1516239022
}) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkZlcmd1cyBJbiBMb25kb24iLCJpYXQiOjE1MTYyMzkwMjJ9.m_N4oysIjlT6r20alHbEvnKMyoLKVB_UKQycf_EPK2w

Pay special attention to the HMAC signature: if you're not familiar with HMAC, it's a method of providing cryptographic assurance that a message has not been tampered with. It's similar to using MD5 hashes to check the integrity of a large file you've downloaded, except it relies upon a secret that only the server has - ensuring the client cannot tamper with the contents of the message.

We can use that signature to not only ensure the integrity of the token though, we can also use it as a unique identifier - safe in the knowledge that a signature will always be unique to one specific JWT token.

Using this 43 character signature as a unique identifier, we can store it against a given user - and do additional checks prior to the standard JWT integrity checks. For instance, in a PHP middleware for a fictitious framework and ORM - we could do something like this:

function jwt_check_token_is_active(Request $req, $next) {
	// for "Bearer <token>", take "<token>"
	$jwtToken = explode(' ', $req->getHeaderLine('Authorization'))[1];

    // Break the JWT Token in to it's constituent parts: Header, Payload, and Signature.
    $jwtParts  = explode('.', $jwtToken);
    $payload   = json_decode( base64_decode( $jwtParts[1] ) );
	$signature = $jwtParts[2];
    
    // Check that the signature matches that stored against the user.
    if ( App::user( $payload->userId )->currentTokenSignature == $signature ) {
		// Continue with the request: most likely doing *real* JWT integrity checks.
		return $next( $request );
    }
    
    throw new Exception("Something fishy is going on here..");
}

Now that's far from production code, and likely not even syntactically valid! It does however demonstrate a point, that by storing the token signature against the user entity, we can revoke tokens by simply removing them from our database/redis/wherever.

Have single tokens for a user? Add a column on to your users table. Have multiple tokens for a user, perhaps for multiple client applications? Store them in a different table, in a 1:many relationship.

But why should I care?

It could take less than 15 minutes to implement the above mechanism, and the benefits are numerous:

  1. Mass token revocation becomes a possibility following a compromise.
  2. Individual token revocation can take place following account hijacks.
  3. Users can properly log out, not simply throw away their credentials.

There are many simple and low-risk scenarios where I can see the appeal of JWT in conjunction with a completely state-less API; but there exists a middle ground whereby solutions like OAuth are overkill, but the benefits of token revocation are still desired. For these situations, a solution like the above can be a simplistic, transparent, but ultimately useful, security layer.


Fergus

Contract Software Developer and DevSecOps Consultant, based out of London in England. Interests include information security, current affairs, and photography.