Bluffers Guide To JWTs

My Top 20 important things about JWTs:

Bluffers Guide To JWTs

Apple [here] Spotify [here]

My Top 20 important things about JWTs:

  1. JWT is a JSON Web Token and is pronounced “jot”. JSON objects support human-readable text and are used in many applications, such as with NoSQL databases.
  2. You should not trust a JWT unless it is cryptographically signed.
  3. For authorization, a captured JWT can be replayed and “played back” to provide a malicious entry or rights into a system.
  4. JWTs should never be trusted before their issue date and their not-before date and never trusted after their expiry.
  5. JWTs have been defined as an RFC standard with RFC7519.
  6. The format is URL friendly and is Base64URL encoded.
  7. A JWT token has three main parameters separated by a period (“.”), and which are the header, the payload and the signature.
  8. The header is typically not encrypted and defines the signature algorithm (“alg”) and the type (“typ”).
  9. The payload is typically not encrypted and uses a Base64 format. The payload can typically be seen by anyone who captures it.
  10. “ey” is a typical field starting part of a parameter in the header and body of a token as ‘{“‘ encoded in Base64 is “ey==”. You can tell if a token is not encrypted with an “ey” as the start of the header and body parameters.
  11. The registered claims of a token are iss (Issuer), sub (Subject), aud (Audience), iat (Issued At), exp (Expires), nbf (Not Before), and jti (JWT ID).
  12. The claim fields are not mandatory and just a starting point for defining claims.
  13. A claim is asserted about a subject, and where we have a claim name and a claim value in a JSON format.
  14. With an HMAC signature, the issuer and validator must share the same secret symmetric key.
  15. If you use HMAC to sign the tokens, a breached secret key will compromise the signing infrastructure.
  16. The two main public key signing methods are RSA and ECDSA.
  17. The time of a token is represented as the number of seconds from 1 January 1970 (UTC).
  18. Each day of a JWT token is represented by 86,400 seconds.
  19. An unsecured JWT does not have encryption or a signature. This is bad! it is represented in the header parameter with an “alg” of “none” and an empty string for the JWS Signature value.
  20. A JWT can be encrypted (but this is optional). For public key methods, we can use either RSA and AES, or we can use a wrapped key.

And a debate I’ve had with many development teams:

What’s a token?

So, what’s a token? Well, it is basically a way to encapsulate data in a well-defined format that has a signature from the issuer. For this, we either sign using HMAC (HMAC-256, HMAC-384 or HMAC-512), RSA signing or ECC signing. The HMAC method requires the use of a secret symmetric key, whilst RSA and ECC use public key encryption. The problem with HMAC is that if someone discovers the secret key, they could sign valid tokens. For enhanced security, we typically use public key encryption, where we sign with a private key and then validate with the public key.

In this case, we will use Google Tink to create JWTs (JSON Web Tokens) and which are signed with elliptic curve cryptography. For this, we will use either NIST P-256 (ES256), NIST P-384 (ES384) or NIST P512 (ES512) for the curves. Overall, we do not generally encrypt the payload in JWT, so it can typically be viewed if the token is captured.

JWT format

A JWT token splits into three files: header, payload and signature (Figure 1).

Figure 1: JWT format

The header parameter

The header contains the formation required to interpret the rest of the token. Typical fields are “alg” and “kid”, and these represent the algorithm you use (such as “ES256”) and the ID, representively. The default type (“type”) will be “JWT”. Other possible fields include “jwk” (JSON Web key), “cty” (Content type), and “x5u” (X.509 URL). An example header for a token that uses ES384 signatures and with an ID of “s5qe-Q” is:

{"alg":"ES384", "kid":"s5qe-Q"}

The payload parameter

The payload is defined in JSON format with a key-pair setting. For a token, we have standard claim fields of iss (Issuer), sub (Subject), aud (Audience), iat (Issued At), exp (Expires At), nbf (Not Before), and jti (JWT ID). The claim fields are not mandatory and are just a starting point — and where a developer can add any field that they want. An example field is:

{"aud":"qwerty", "exp":1690754794, "iss":"ASecuritySite", 
"jti":"123456", "sub":"hello"}

The time is defined in the number of seconds past 1 January 1970 UTC. In this case, 1690754794 represents Sunday 30 Jun 22:06:34:

The token signing parameter

There are two ways to sign a token: with an HMAC signature or with a public key signature. With HMAC, we create a shared symmetric key between the issuer and the validator. For public key encryption, we use either RSA or ECDSA. For these, we create a signature by signing the data in the token with the private key of the creator of the token, and then the client can prove this with the associated public key. For public key signing, the main signing methods are:

  • ES256. ECDSA using NIST P256 with SHA-256.
  • ES384. ECDSA using NIST P384 with SHA-384.
  • ES512. ECDSA using NIST P512 with SHA-512.
  • RS256. RSASSA-PKCS1-v1_5 with the SHA-256 hash.

and for HMAC:

  • HS256. HMAC with SHA-256.
  • HS384. HMAC with SHA-384.
  • HS512. HMAC with SHA-512.

In public key signing, we have a key pair to sign the token:

And with HMAC, we share a secret signing key:

Encrypting the payload

A JWT can be encrypted, but this is optional. For public key methods, we can use either RSA and AES or a wrapped AES key. An “alg” method of “RSA1_5” will use 2,048-bit RSA encryption with RSAES-PKCS1-v1_5, “A128KW” will use 128-bit Key Wapping and “A256KW” uses 256-bit Key Wapping. With key wrapping, the private key is encrypted with a secret key. Both the issuer and verifier will know this secrete key.

For symmetric key methods, we can use “A128CBC-HS256” (AES-CBC) and “A256CBC-HS512” (HMAC SHA-2). It is possible to also use ECDH-ES (Elliptic Curve Static) for key exchange methods

An example token

An example token is:

eyJhbGciOiJFUzI1NiIsICJraWQiOiJ3WHd6dVEifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzU0Nzk0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.cAXunJHLRrqFfJStJTFlwkUTze6K8EpwOui9abDeiSBcR5WeOEpXCSUQBnS_VdVnLsmVV2AWUX0kOTqIWERcMQ

We then have:

  • Header: eyJhbGciOiJFUzI1NiIsICJraWQiOiJ3WHd6dVEifQ
  • Payload: eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzU0Nzk0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ
  • Signature: cAXunJHLRrqFfJStJTFlwkUTze6K8EpwOui9abDeiSBcR5WeOEpXCSUQBnS_VdVnLsmVV2AWUX0kOTqIWERcMQ

These are in Base64 format, and we can easily decode the header as:

{"alg":"ES256", "kid":"wXwzuQ"}

and the payload as:

{"aud":"qwerty", "exp":1690754794, "iss":"ASecuritySite", 
"jti":"123456", "sub":"hello"}

The signature value will be in a byte array format.

Sample code

With Google Tink, we can create a token with the fields using:

 expiresAt := time.Now().Add(time.Hour)
subject:= "CSN09112"
audience := "Sales"
issurer := "ASecuritySite"
jwtid := "123456"

rawJWT, _ := jwt.NewRawJWT(&jwt.RawJWTOptions{
Subject: &subject,
Audience: &audience,
Issuer: &issurer,
ExpiresAt: &expiresAt,
JWTID: &jwtid,
})

Next we will generate an ECC private key using either NIST P256, NIST P-384 or NIST P-512. In the following, we create a private key (priv) and which will be used to sign the token:

priv,_ =keyset.NewHandle(jwt.ES256Template()
signer, _ := jwt.NewSigner(priv)
token, _ := signer.SignAndEncode(rawJWT)

We can then create the public key from the private key, and validate the token with this key:

pub, _:= priv.Public()
verifier, _ := jwt.NewVerifier(pub)

The full code is [here]:

package main
import (
"fmt"
"time"
"os"
"strconv"
"github.com/google/tink/go/jwt"
"github.com/google/tink/go/keyset"
"github.com/google/tink/go/insecurecleartextkeyset"
)
func main () {
priv, _ := keyset.NewHandle(jwt.ES256Template())

expiresAt := time.Now().Add(time.Hour)
subject:= "CSN09112"
audience := "Sales"
issurer := "ASecuritySite"
jwtid := "123456"
t:=0
argCount := len(os.Args[1:])
if (argCount>0) {subject= string(os.Args[1])}
if (argCount>1) {audience= string(os.Args[2])}
if (argCount>2) {issurer= string(os.Args[3])}
if (argCount>3) {jwtid= string(os.Args[4])}
if (argCount>4) {t,_ = strconv.Atoi(os.Args[5])}
switch t {
case 1: priv,_ =keyset.NewHandle(jwt.ES256Template())
case 2: priv,_ =keyset.NewHandle(jwt.ES384Template())
case 3: priv,_ =keyset.NewHandle(jwt.ES512Template())
}
pub, _:= priv.Public()

rawJWT, _ := jwt.NewRawJWT(&jwt.RawJWTOptions{
Subject: &subject,
Audience: &audience,
Issuer: &issurer,
ExpiresAt: &expiresAt,
JWTID: &jwtid,
})
signer, _ := jwt.NewSigner(priv)
token, _ := signer.SignAndEncode(rawJWT)

verifier, _ := jwt.NewVerifier(pub)

validator, _ := jwt.NewValidator(&jwt.ValidatorOpts{ExpectedAudience: &audience,ExpectedIssuer: &issurer})
verifiedJWT, _:= verifier.VerifyAndDecode(token, validator)

id,_:=verifiedJWT.JWTID()
sub,_:=verifiedJWT.Subject()
aud,_:=verifiedJWT.Audiences()
iss,_:=verifiedJWT.Issuer()
at,_:=verifiedJWT.IssuedAt()
ex,_:=verifiedJWT.ExpiresAt()

fmt.Printf("Public key:\t%s\n",priv)
fmt.Printf("Public key:\t%s\n\n",pub)
fmt.Printf("Token:\t%s\n\n",token)

fmt.Printf("Subject:\t%s\n",sub)
fmt.Printf("Audience:\t%s\n",aud)
fmt.Printf("Issuer:\t\t%s\n",iss)
fmt.Printf("JWT ID:\t\t%s\n",id)
fmt.Printf("Issued at:\t%s\n",at)
fmt.Printf("Expire at:\t%s\n",ex)

fmt.Printf("\n\nAdditional key data\n")
exportedPriv := &keyset.MemReaderWriter{}
insecurecleartextkeyset.Write(priv, exportedPriv)
fmt.Printf("Private key: %s\n\n", exportedPriv)
exportedPub := &keyset.MemReaderWriter{}
insecurecleartextkeyset.Write(pub, exportedPub)
fmt.Printf("Public key: %s\n\n", exportedPub)
}

A sample run proves the process [here]:

Public key:	primary_key_id:1926408156  key_info:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey"  status:ENABLED  key_id:1926408156  output_prefix_type:TINK}
Public key: primary_key_id:1926408156 key_info:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey" status:ENABLED key_id:1926408156 output_prefix_type:TINK}
Token: eyJhbGciOiJFUzI1NiIsICJraWQiOiJjdEtuM0EifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzUxNTI0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.qfui2u9hBpEgiQQeeWNJtSanyl4rbYkViIZJxVmBvCsP72ovcT20qC35YbQOh7Q8cCqA37Fk8OXWSQ-geg6E-Q
Subject: hello
Audience: [qwerty]
Issuer: ASecuritySite
JWT ID: 123456
Issued at: 0001-01-01 00:00:00 +0000 UTC
Expire at: 2023-07-30 21:12:04 +0000 GMT

Additional key data
Private key: .{primary_key_id:1926408156 key:{key_data:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey" value:"\x12F\x10\x01\x1a \xcdPtI\x03)\xb0\xf7H9'\x1e\x94t\xaaa\x99\xf8ڍv\xcf\xd6|\x1a\x1aV6H!\xda\x00\" \xc2ϥ\xfaD\x16\xb2\xfa\xd7\x00\xfe\xba\xe4\xf3\xed%\x03\x9a^\x1d\x9f\x93_\xf3\x1f\xd9W\x90\x8aâX\x1a p\xf7,_}\x13\xff\x84\x9c\xc6j\xdaͯ\xc7\x1b.\xb2|\x19؎\xfb\xa9j\x05\xb3NF\xc4\x7f\xcc" key_material_type:ASYMMETRIC_PRIVATE} status:ENABLED key_id:1926408156 output_prefix_type:TINK} .nil.}
Public key: .{primary_key_id:1926408156 key:{key_data:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey" value:"\x10\x01\x1a \xcdPtI\x03)\xb0\xf7H9'\x1e\x94t\xaaa\x99\xf8ڍv\xcf\xd6|\x1a\x1aV6H!\xda\x00\" \xc2ϥ\xfaD\x16\xb2\xfa\xd7\x00\xfe\xba\xe4\xf3\xed%\x03\x9a^\x1d\x9f\x93_\xf3\x1f\xd9W\x90\x8aâX" key_material_type:ASYMMETRIC_PUBLIC} status:ENABLED key_id:1926408156 output_prefix_type:TINK} .nil.}

Conclusions

There are many risks in using JWTs, especially in capturing a token and playing it back. The expiry date should thus be set so that it would limit the impact of any malicious use.

Using public key encryption to sign JWTs is a good method, as the authenticity of the token can be proven with the associated public key. With an HMAC method, we need to share a secret key, which could cause a data breach.

And, finally, which signature method should you pick? Find out here: