Niraj Zade   Home  Blog  Tools 

JWT

Tags: auth  

JWT is a self-contained blob of information that has been signed by the creator, and can be self validated (using signatures). It is majorly used by distributed auth systems.

It has in-built tamper detection. You will know when the client gives your systems a tampered token instead of the token you original token.

If you work in web development, you must have seen JSON Web Tokens (JWT) everywhere. For example, this is a JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVC
J9.eyJzdWIiOiIxMjM0NTY3ODkwIiwib
mFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxN
TE2MjM5MDIyfQ.SflKxwRJSMeKKF2
QT4fwpMeJf36POk6yJV_adQssw5c

Anatomy of a JWT

A JWT has 3 parts: header, body and signature. They are concatenated by a period - .

header.payload.signature

Encoding

JWT are json, but they are encoded as base64url strings.

Note: base64url and base64 are different things. You cannot call base64url as base64. All those posts you read that call the encoding as base64? wither they never read the RFC, or they forgot.

Base64url

JWT uses base64url. It has some differences than the usual base64. The output of base64url encoding has to be safe for use in 2 places:

urls,
filenames.

Base64 has two characters that aren’t safe for these purposes: + and /. So, base64url got rid of them, and replaced them with - and _ respectively.

The other alphanumerics from character number 0 to 61 are the same. To check if a string is base64url compatible, use the regex: ^[a-zA-Z0-9_-]+$

Here is the RFC that defined this: LINK: RFC 4648

Here is the entire base64 encoding table:

index   char    bae64url_char
0   A   A
1   B   B
2   C   C
3   D   D
4   E   E
5   F   F
6   G   G
7   H   H
8   I   I
9   J   J
10  K   K
11  L   L
12  M   M
13  N   N
14  O   O
15  P   P
16  Q   Q
17  R   R
18  S   S
19  T   T
20  U   U
21  V   V
22  W   W
23  X   X
24  Y   Y
25  Z   Z
26  a   a
27  b   b
28  c   c
29  d   d
30  e   e
31  f   f
32  g   g
33  h   h
34  i   i
35  j   j
36  k   k
37  l   l
38  m   m
39  n   n
40  o   o
41  p   p
42  q   q
43  r   r
44  s   s
45  t   t
46  u   u
47  v   v
48  w   w
49  x   x
50  y   y
51  z   z
52  0   0
53  1   1
54  2   2
55  3   3
56  4   4
57  5   5
58  6   6
59  7   7
60  8   8
61  9   9
62  +   -
63  /   _
        = (optional character)

Size limit

A JWT by itself can be infinitely large. You are only limited by the limits of your application.

In the context of this note, we are talking about HTTP authentication-authorization. So, we are limited by the size limit of the HTTP headers, which is typically 8kb. So practically, max size the your finally generated base64 encoded autn token is 8 kb. This can increase in the future when browsers raise the limit.

Think of it like this: you can build an infinitely large car, but you are limited by the size of the roads you’ll drive it on.

JWT cryptography

This is important to understand when you are working across languages.

Eg: you may generate my tokens on the backend in go, and then verify them on the frontend in javascript. If something gets screwed, you need to know exactly what went wrong, and in which step.

Obligatory advice: Don’t roll your own crypto library. Use a trusted library written by experts.

There are 3 distinct stages:

  1. Generating input
  2. Generating signature
  3. Verifying the token against the signature

For the cryptography part, we have 2 distinct options:

  1. Symmetric Algorithm. Token generation and verification will be done using the same keys. This uses the HMAC algorithm
  2. Asymmetric Algorithm: Token generation will be done with a private key, but validation will be done with a public key. This uses RSA or ECDSA algorithm.

These two classes of algorithms have two distinct flows for generating and verifying the JWT. I'll be explaining these flows, and not the algorithm themselves.

Symmetric Algorithm - HS256

This uses the HMAC algorithm (Hash message based authentication code). You will specifically see the HS256 variant being used a lot in your token softwares.

HS256 Signature Generation

The steps to generate the signature is pretty straightforward:

  1. Encode header into base64url encoding
  2. Encode body into base64url encoding
  3. Concatenate headerbase64url and payloadbase64url using a .
  4. Take the securely stored secret and append it to the concatenatedheaderand_payload
  5. Calculate SHA256 (or SHA512) of this string. Encode it using the base54url encoding. This is the token's signature.
  6. Generate the token as: header.payload.signature
header = {"alg": "HS256","typ": "JWT"}
header_base64url_encoded = base64UrlEncode(header)
= eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

payload = {
  "Issuer": "deeperdev.com",
  "Issued At": "2023-04-03T15:23:13.120Z",
  "Expiration": "2023-04-03T15:23:13.120Z",
  "Username": "deeperdev",
  "Role": "Admin"
}
payload_base64url_encoded = base64UrlEncode(payload)
= eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0

concatenated_header_and_payload = header_base64url_encoded + payload_base64url_encoded
= eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0

secret = superdupersecret

signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

final_token = concatenated_header_and_payload . signature
final_token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0.E7RskXPu7CYT4SzgsfAjC8jqfhRqbrfU0iVOv7vX2Lw

HS256 Signature Verification

When the server recieves a JWT, it repeats the same procedure as the creation.

  1. Take the base64url encoded header and payload concatenated with a .
  2. Append the secret stored in this server to the string
  3. Pass this string through the SHA256 algorithm
  4. Compare the hash function's output with the signature of the received token
  5. If the signatures match, declare the token as valid. Otherwise declare it as invalid.
recieved_token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0.E7RskXPu7CYT4SzgsfAjC8jqfhRqbrfU0iVOv7vX2Lw

recieved_token_signature = E7RskXPu7CYT4SzgsfAjC8jqfhRqbrfU0iVOv7vX2Lw

concatenated_header_and_payload = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0

secret = superdupersecret
calculated_signature = HMACSHA256(concatenated_header_and_payload, secret)

// if calculated_signature == recieved_token_signature, then token is valid
// otherwise, token is invalid

HS256 is a symmetric algorithm. It means that the secret key used to create the token has to be used to verify the token. So, this algorithm is suited in single node setups, where the token issuer and verifier is the same. Why? because as the encoder and decoded both need to have the same secret, you will have to set up secure transport and storage on all of those nodes.This is a security risk. One bad server, and your entire auth system is compromised. Also, rolling a secret key distributed over multiple nodes is a pain. (I hope you periodically roll your keys in production). During creation, the issuer takes the base64url encoded header and payload, concatenated with a . and appends the secret to it. It then hashes this resultant string with SHA256.

Problems with HS256

Here is the problem: One key does everything. A single key is used to do both signing and validation.

Need complete trust on other nodes in the auth setup

You need to have complete trust over every node in your auth setup. Now, if you want to give some auth server ability to verify tokens, you have to give it the key. Now, since they have the key, they can verify the tokens. But they can also use the key to sign tokens and pretend that they are you. This is terrible for security, as you have to trust everyone.

So, you absolutely cannot use HS256 when you are sharing your auth verification ability with third parties (outside your company)

Larger surface area for breaches

The surface area of attack increases with every server you add to the auth fleet.

Suppose you have 10 nodes in your auth setup. All 10 modes have the key that can do the verification and signing. Now, if any single node is breached, your entire auth setup has been breached. The intruder can freely generate tokens & sign them, and your entire system will accept those intruder's tokens.

Asymmetric Algorithm - RS256

RS256 is an asymmetric algorithm. So there is a pair of keys: a private key, and a public key. The private key is used to create and sign the token. The public key is used to verify the token.

RS256 signature generation

During creation, the issuer takes the base64url encoded header and payload, concatenated with a . and hashes it using SHA256. Then it encrypts this hash using the RSA algorithm, using its private key. This encrypted output is base64url encoded, and this is the signature.

The keys may look like this:

private_key = 
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCo63qFnHa98QpE
+w7lxBOQxE0MxAzlQ21GuKRBfnu+nOG94vxJjE8FrpHVh6jBPhBwuLjd9lz07XTW
sJ0gukhvuz/sxcvkyCccel5Ak8S6dW8+I35IOsz1Opx9QOod9IeefCHu+HjoNppE
PjT3uxGwGC4/HRs1d6oZ1qpNKqc8eoDmiG7P1s8gI+nByFAYTnBGK2iujamrE9hj
1xm10t0LVyFew1vw/V9fsGGRP8rgsGUxJRvQmTNYRQ0pjw8SqNgEyJCtsybHdDsA
ouIuzjjlRxLWRuItxO9Gvj0rZgObtULWJmx8rluBu9+FBJHydVCTmFIR6jbWmJWn
/z14W6CZAgMBAAECggEAGl8U9DoOoa25bDaDx2Q6p721x7ntx3ck3schwaXU/Ney
OHpw56yTg7ASzXLN6klduLNmDSUSsxxFQuU0yrC6cVMa8kSZBeEnlf8Wqt9G9dMy
qFFTPESNzfU2DCnvwvhzmc8IXy4EdkBcCi5qB4j5hHPp+Gl7X3gMotcMJFr12++F
IzAHyFTn296m05WHVxzmdZ5+njgbqs6/5CnROuc9VyOJqHuQ8m5xfIk68+rA1FUr
Nic8uHuTJFIWc2yEIm0P1CAQdt92ER6BC4rbmC8CIh8Sw93oMeYKTyp0++jEmL85
lc6x02vaB9mpAFU9OWFMxwsntiMLUpO0jYk8QXYTTwKBgQDZy+tgCN5C0aKIMlPv
4PmqGVXc61pcHN1tDbC7jHFiqRKo1872x8wZZHigfmoPSuccMem5yWo9mKvJpBuL
q7wH9IilffQN074cieLOA3NHwGudam7nd9gZ7jw/ycKyJavIaH9Mv240CsLy2og7
86VccXbVq2nI1QHHZJyYt8uQewKBgQDGjMQ9sdLZefDxnI58phEqi6OWBza1aAU8
aYknCQ4Ar5WuSKW4F6FxiwG43PKQImctd1IHpcUrRT3LJWW2fvPcu1gpXVx3lBry
1shOpFDJwg692Gq/53kOnPU7T6d76I/BovpDwPY027np04a31gpV7lCBj1RE4ayl
Re4juklo+wKBgD5neWeo4tZObr2LfhVrZt3gKIQGQ3vFIYTPuWXjldFpFFmgjEKV
eNuFuDJ0RjtfgNzJSGjdVz2S8xXxmZrpeBTncgfJERatJvnSYFQbFProHW2bQ2+7
HQZBBq5YRxr4REJF/sOkzhTHSJiBGSvkYesc76nFVagfsETLhTsU3pTlAoGAdRsF
i9XcJMUVZYwPRlm0ekGOJKjwjaJipDUi3dErXyAwynCyvZfCcvOn+l7m+jgwXtKn
oTcWyeS3A6B1E2RhdOlSoGMebLEADAa+chPcSoOYqkSBAdsGvaW5xle+0whh8bWs
olWnYZnPV6iZJsipo/FBrojZDR+F8p0CTjRLpA0CgYA7dPBKe90lLqOVbGs/5RkY
HelY5JMgJo4mQK3aS68hxsrn1Gjg3W37OAtcRazywWGD6PxIAkJYUwpMFXfoghFW
B55OIlnZNqZzyL6lHtSA8ta25y2Rtgnn2dXBs/BwN/CKVXXWiHAESdPghawnz5dP
+pqjgvXQ5/nXnPqUcL4rkQ==
-----END PRIVATE KEY-----

public_key = 
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqOt6hZx2vfEKRPsO5cQT
kMRNDMQM5UNtRrikQX57vpzhveL8SYxPBa6R1YeowT4QcLi43fZc9O101rCdILpI
b7s/7MXL5MgnHHpeQJPEunVvPiN+SDrM9TqcfUDqHfSHnnwh7vh46DaaRD4097sR
sBguPx0bNXeqGdaqTSqnPHqA5ohuz9bPICPpwchQGE5wRitoro2pqxPYY9cZtdLd
C1chXsNb8P1fX7BhkT/K4LBlMSUb0JkzWEUNKY8PEqjYBMiQrbMmx3Q7AKLiLs44
5UcS1kbiLcTvRr49K2YDm7VC1iZsfK5bgbvfhQSR8nVQk5hSEeo21piVp/89eFug
mQIDAQAB
-----END PUBLIC KEY-----

Here is the signature generation pseudocode:

header = {
  "alg": "RS256",
  "typ": "JWT"
}
header_base64url_encoded: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9

payload = {
  "Issuer": "deeperdev.com",
  "Issued At": "2023-04-03T15:23:13.120Z",
  "Expiration": "2023-04-03T15:23:13.120Z",
  "Username": "deeperdev",
  "Role": "Admin"
}
payload_base64url_encoded = eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0

concatenated_header_and_payload = header_base64url_encoded + "." + payload_base64url_encoded
= eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

hash = SHA256(concatenated_header_and_payload)

signature = RSA_encrypt(hash, private_key)

generated_token = concatenated_header_and_payload + "." + signature
= eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJJc3N1ZXIiOiJkZWVwZXJkZXYuY29tIiwiSXNzdWVkIEF0IjoiMjAyMy0wNC0wM1QxNToyMzoxMy4xMjBaIiwiRXhwaXJhdGlvbiI6IjIwMjMtMDQtMDNUMTU6MjM6MTMuMTIwWiIsIlVzZXJuYW1lIjoiZGVlcGVyZGV2IiwiUm9sZSI6IkFkbWluIn0.k1pRAD8FfkbfyzX8c4btLassYqBwJJvrDrQsc3TKFWXhbTjnUAp_4DpiJHZ1DcQM8Ca8-CV79f5O8J2I0MpLTdh1eZpX-ouE76Dv0a90ejcfkVjHf77jjo0BCco0KRSaX1RZgWPj5u0yqejnCdAoJoGHRFX0ryg-RMcTt1z0VrHfRmxX0emnc1sBDoeeFSukEjfL2uGdLrOOL3mGxld-gzixxWniTKua7DIUzixRxawJ5EqTOOFNLbBzHjhUQe3VbLMK0WP2jIarL5wS4Ds9BKlY0yl7GNp5BQJZiSgcfA2EN5GnqVOVs-MenEGhNxbbr6oj0EUgz7ZkczUoug4Zuw

RS256 signature verification

  1. The verifier again takes the recieved base64url encoded header and payload, concatenated with a . and hashes it using SHA256.
  2. Then to verify this hash, the token in the signature is decoded using the public key.
  3. If both hashes match, then the token is valid. Otherwise, it is invalid.

Advantages of RS256 over HS256

In HS256, a single key does everything. Sharing the key to give a server the ability of verifying tokens also means giving the server the power to sign new tokens. This is not the case in RS256.

No need to have trust

Only the private key can be used to sign the tokens. So your primary nodes get the private key. All the other verifier nodes will only get the public key. The public key can only validate tokens signed with the private key. New tokens cannot be signed with the public key.

So now, you can freely give the power to verify tokens to anyone: your own servers, third party servers etc. You don't need to have trust in them. Even if the public key leaks, it is useless. The attacker can only verify new tokens, not create new ones. Your auth system is safe.

Smaller surface area for breaches

You only truly need to protect servers with the private key, which will be a very small fleet of servers. So, your surface area of breaches got massively reduced.

Choosing an algorithm

Single node auth setup

If you are using a single server for all auth (generation+and verification), HS256 is fine. You have to protect the secret on only one server.

Multi-node auth cluster

If you have multiple auth servers, exclusively use RS256. Because if one server leaks the secret, then your entire auth fleet is compromised. The attacker can immediately start generating tokens which will be accepted as vaild by all of your auth servers.

Stick to using RS256 in multi-node auth setups. If a verifier node leaks the keys, it will just be a public key. Public key cannot be used to generate valid tokens. So your auth cluster is still safe.

If your private key gets leaked, then now your auth setup is truly compromised. Change the keys and update your private keys in the verifier nodes.

My personal recommendation

Just stick to using ES256 in all setups.

Why? because decisions have a tendency to not change.

Eg: Suppose you rolled out a single node HS256 auth setup. and moved on to other systems. Later, some person comes in to solve the single auth server bottleneck. They will most probably end up creating a multinode setup, still using HS256.

It is much more brainless and secure to use RS256 from the get go, and let the system stay secure by default, so it can scale in the future without needing yet another decision about changing the algorithm.

Always choose to be secure by default.

JWT vulnerabilities

JWT spec itself is logically rock solid. But the implementations may have holes. There is one specific famous hole:

CVE-2018-1000531

This was one weird JWT exploit. It is very funny actually.

In JWT's header field, we tell the algorithm used to create the token’s signature, which the receiver will also use to verify the token’s signature. So the exploit is simple. The attacker takes your token, and sets the header’s algorithm field to none. Now, the verifying server sees that the algorithm is set to none, so it doesn’t verify the token. Such a simple exploit!

The fix is also simple: configure your server to discard all tokens which don’t have a valid algorithm set in the header’s algorithm field.

JWT performance

Multi node? Use RS256. The performance gains from HS256 aren’t worth the security risks.

  • HS256 uses RSA256 algorithm, which is very fast. So token generation and verification is going to be very fast.
  • RS256 is comparatively slower (although not by much).

So, single auth node? you can get by with HS256.

By default, stick to RS256

Ending notes

I haven't covered ECDSA (Elliptic Curve Digital Signature Algorithm). It is an asymmetric encryption algorithm, and is the most secure of them all.