Is JWT good idea for authorization and authentication

feather icon

This article was originally published on 26th December 2022. Some of its contents may not be relevant anymore.

without avatar

Rafał Teron

Backend Developer

2023-01-04

#Tech

In this article

Intro

How does JWT work?

Is JWT secure?

Local storage vs cookies

Summary

Overview

JWT has been widely used in web development for a long time. Many developers have used it to replace stateful methods of authenticating users, such as classic file sessions. Unfortunately, it is common to use it without understanding its consequences and the differences between JWT and other methods for authenticating and authorizing requests.

How does JWT work?

The JSON Web Token structure is described in its specification. There are two main forms of JWT:

  • JWS (JSON Web Signature): more commonly used. This type of token contains a publicly available header and claims (payload), secured by a signature.
  • JWE (JSON Web Encryption): a less known version of JWT that is fully encrypted. You can read more about its encryption in the JWA (JSON Web Algorithms) specification.

There are many articles explaining the differences between JWS and JWE, and other technical aspects of JWT structure, for example:

You can try live JWT encoder/decoder on jwt.io.

Facing security challenges?

contact box image

Is JWT secure?

As always, it depends. JWT is simply a way to sign or encrypt data, and its security fully depends on the context and implementation.

Common problems with JWT:

  • Implementation issues:
    • Not restricting the algorithm and key used to decode the token
    • Invalid signature comparison, leading to timing attacks
    • Allowing tokens signed with public keys when using asymmetric algorithms
  • Other issues:
    • Not using sufficiently long secrets
    • Lack of token invalidation
NOT RESTRICTING THE ALGORITHM USED TO DECODE THE TOKEN

JWT contains the algorithm type used in its header. This header may be used by decoding libraries to determine which algorithm should be used to decode it. This behavior may be useful, for example, when validating a token from an unknown source, but it should be absolutely avoided when validating tokens with our secret key.

Some libraries, by default, can accept any algorithm (you can find the full list of supported algorithms in the JWA reference). Most libraries should prevent accidentally accepting unsigned tokens (tokens with no algorithm); the jsonwebtoken library is an example.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { verify, sign } = require("jsonwebtoken");
 
const SECRET = "SECRET";
const payload = { dev: "deliver" };
const signedToken = sign(payload, SECRET, { algorithm: "HS512" });
const notSignedToken = sign(payload, undefined, { algorithm: "none" });
 
console.log(`Signed token: ${signedToken}`); // Signed token: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJkZXYiOiJkZWxpdmVyIiwiaWF0IjoxNjY5OTk3MTU1fQ.MB05AlGfd3pNEfiW5p7HjvGm3wQ6hDT0Jh5Sjk0Zls4xsY0uGvfzwDJu03_ja76J2vEDJW3kN9pohB0TktKaGg
console.log(`Not signed token ${notSignedToken}`); // Not signed token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkZXYiOiJkZWxpdmVyIiwiaWF0IjoxNjY5OTk3MTU1fQ.
 
console.log(verify(signedToken, SECRET)); // { dev: 'deliver', iat: 1669996957 }
console.log(verify(notSignedToken, SECRET)); // throws JsonWebTokenError: jwt signature is required
 
// even though library prevents us from accidentally doing this mistake, we should still strict algorithms that could be used to verify the key
console.log(verify(signedToken, SECRET, { algorithms: ["HS512"] }));

In many libraries, verifying and decrypting are two different functions! You should always make sure that you are using the function that actually checks the signature.

INVALID SIGNATURE COMPARISON ALLOWING TIMING ATTACKS

When comparing signed keys, you should always use functions designed for cryptographic comparisons. It is very easy to accidentally write code that is vulnerable to timing attacks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// valid code
const { verify } = require("jsonwebtoken");
console.log(verify(signedToken, SECRET, { algorithms: [ALGORITHM] }));
 
// invalid code
const { sign, decode } = require("jsonwebtoken");
const { createHmac } = require("crypto");
 
const SECRET = "SECRET";
const originalPayload = { dev: "deliver" };
const signedToken = sign(originalPayload, SECRET, { algorithm: "HS512" });
 
const { header, payload } = decode(signedToken, { complete: true });
const hmac = createHmac("sha512", SECRET);
const content =
  Buffer.from(JSON.stringify(header)).toString("base64url") +
  "." +
  Buffer.from(JSON.stringify(payload)).toString("base64url");
const signature = hmac
  .update(Buffer.from(content, "utf-8"))
  .digest("base64url");
const expectedToken = content + "." + signature;
 
if (expectedToken.length !== signedToken.length) {
  throw new Error("invalid signature");
}
 
for (let i = 0; i < expectedToken.length; i++) {
  if (expectedToken[i] !== signedToken[i]) {
    throw new Error("invalid signature");
  }
}

In this example, the custom function seems very unconventional, but there are libraries where valid usage is not that obvious. This is also very important when creating our own JWT library implementation. Please check out this awesome article about JWT timing attacks available at the following link.

ALLOWING TOKENS SINGED USING PUBLIC KEYS WHEN USING ASYMMETRIC ALGORITHMS

At this moment, I don’t know of any implementation with this problem, but this may occur, for example, when we accidentally allow tokens to be verified with both asymmetric and symmetric algorithms while exposing the public key. Exposing the public key is a common technique that allows third parties to verify tokens signed by our site. With this implementation flaw, a token signed using the allowed symmetric algorithm and exposed public key would be successfully verified. Fortunately, it is quite difficult to accidentally make this mistake.

NOT USING SUFFICIENTLY LONG SECRETS

You can try to find a valid secret for any JWT using popular tools like John the Ripper, or Hashcat, or create your own tool. However, for professional use, you usually want to rely on existing tools. A secret that is not long enough could be cracked fairly easily. You can find a lot of tutorials about JWT cracking, including:

The problem is very easy to avoid - all you have to do is use secure secrets.

TOKEN REPLAYS AND INVALIDATION

Tokens usually use the exp (Expiration Time) claim, which indicates when the token should no longer be valid. Often, this claim is used alongside the nbf (Not Before) claim, which indicates when the token starts being valid. Using these two claims allows you to specify a time window for the token’s validity. Within this period, the token can typically be used as many times as needed. This creates a problem when we want to invalidate the token, for example, because:

  • We want to log out the user
  • The token has been stolen
  • The token's claims are no longer up to date

In a fully stateless architecture, we are not able to perform any of these actions instantly. We can only create new tokens, so once the token expires, we can replace it with a new one. The most popular workaround is to use short-lived tokens and force the client to refresh them (for example, using a separate token whose only role is to allow the creation of a new ‘main’ token). Unfortunately, in many cases, this is not enough, as there are situations where we don’t want the token to remain valid even for a few minutes longer.

Local storage vs cookies

Similar to sessions, JWT can be kept either in a place where JS can access it and append it to a request (for example, as a header) or in a cookie (preferably HTTP-only), which can be read by the server. The most common way of sending JWT is to store it in local or session storage and send it to the server as an Authentication header. Many people argue that this is the best method, as it prevents CSRF attacks. Unfortunately, storing the token in local storage allows a potential attacker to steal its content using an XSS attack, so in most cases, you should still consider placing the token in an HTTP-only cookie managed by the server and using a separate CSRF token when required. In my opinion, JWT should not be that widely associated with headers, as the JWT RFC does not say anything about their transfer.

Summary

JWT can be used for keeping user information as a client-side session, but if you want to build a secure implementation, you must have a way to instantly invalidate the token so that these tokens won’t be truly stateful. In my opinion, in most cases, this procedure is more complicated than standard sessions; therefore, I would opt for session usage when possible. JSON Web Token was not designed to be the medium for this kind of information but instead has a lot of possible use cases where stateful solutions would be impossible (for example, asynchronous offline systems).

without avatar

Rafał Teron

Backend Developer

Share this post

Related posts

Tech

2023-10-11

Improving compatibility of registry cache with GitLab Container Registry

Previous

Next

Want to light up your ideas with us?

Józefitów 8, 30-039 Cracow, Poland

hidevanddeliver.com

(+48) 789 188 353

NIP: 9452214307

REGON: 368739409