Skip to content
Chapter 38 · Spring Boot

Spring Security Part 5: JWT Explained ​

Basic authentication (part 4) sends the password with every request. A token avoids that: the user logs in once, then sends a token that proves who they are. This part explains the most common token format, the JWT; part 6 implements it.

A token is trusted because it is signed. Two kinds of signing appear below. Symmetric signing (HMAC) uses one secret key to both sign and verify, so only parties holding the secret can check a token. Asymmetric signing (RSA and similar) uses a private key to sign and the matching public key, which anyone may have, to verify. A signature proves the data has not changed since it was signed; it does not hide the data.

What is a JWT? ​

A JWT (JSON Web Token) is a secure way of transmitting information between parties as a JSON object. The information can be verified, because it is digitally signed, with a secret (HMAC) or a private key (RSA, ...).

It was designed simply as a way to pass information securely, but today it is used mainly for:

  • Authentication: confirming who the user is.
  • Authorization: checking what the user may access.
  • SSO (single sign-on): log in once, use several applications. JWTs are a common way to implement it, for example as OpenID Connect's ID tokens.

The flow ​

  1. The client logs in with a username and password at an authentication server. This can be your own application or a third-party service that issues and validates tokens, so you need not build that logic yourself.
  2. The server returns a JWT.
  3. The client calls your API (the resource server) with the token in the Authorization header.
  4. The resource server verifies the token itself: it checks the signature with the shared secret or the issuer's public key, then the expiry. It does not need to call the authentication server or a database for each request; that is the point of a self-contained token. (Some setups do ask the issuer on each request, called token introspection, but that is typical for opaque tokens, not JWTs.)
  5. If the token is valid, the request is served.

Before JWT: session ids ​

The session id (for example JSESSIONID) is just a random string. All the information (user, roles, expiry) lives in the server's store, so:

  • It is stateful: it depends on server-side state, which is awkward in a distributed system. Every server, or every database replica, must see the same sessions, since any server may get the next request.
  • Every request needs a lookup in the database to turn the id back into user information, adding latency.

A JWT carries the information itself (user id, roles, expiry), so no lookup is needed: it is stateless.

Structure ​

A JWT is three parts separated by dots:

xxxxx.yyyyy.zzzzz
header.payload.signature

Metadata about the token:

json
{ "alg": "HS256", "typ": "JWT" }
  • typ: the token type, practically always JWT.
  • alg: the signing algorithm, such as HS256 (HMAC with SHA-256, symmetric) or RS256 (RSA, asymmetric).

Payload ​

The claims: information about the user and anything else you add. Three kinds:

  • Registered claims: names reserved by the JWT standard, with fixed meanings:
    • iss (issuer): who issued the token
    • sub (subject): who the token is about, the user
    • aud (audience): who the token is intended for
    • exp (expiration time): not valid after this
    • nbf (not before): not valid before this
    • iat (issued at): when it was issued
    • jti (JWT id): a unique id for this token
  • Public claims: custom names meant to be understood by several parties, such as email or country.
  • Private claims: custom names for internal use between specific parties only, not standardized; other services are not expected to understand them.
json
{
  "iss": "https://example.com/auth",
  "sub": "1234567890",
  "exp": 1711945909,
  "email": "john@xyz.com",
  "jti": "unique_id_12312312"
}

Never put confidential data (such as a password) in the payload. As shown below, anyone can read it.

Signature ​

  1. Encode the header and the payload separately with Base64URL (a URL-safe variant of Base64).
  2. Join them with a dot: encodedHeader.encodedPayload. This is the message.
  3. Sign the message with the algorithm from the header:
    • HMAC (symmetric): with a secret key. The same secret signs and verifies.
    • RSA (asymmetric): with the issuer's private key; anyone with the public key can verify.
  4. Base64URL-encode the signature and append it: encodedHeader.encodedPayload.encodedSignature.

For example, the header above encodes to eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, which is why so many JWTs start with eyJ.

To verify, the receiver recomputes the signature over the first two parts and compares. If anyone changed a single character of the header or payload, the signature no longer matches.

Strictly, a signed JWT is a JWS (JSON Web Signature). Since practically every JWT in use is signed, "JWT" and "JWS" are used interchangeably.

Sending it ​

curl --request GET 'https://exampleHost.com:12345/api/resource' \
     --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'

Credentials always go in the Authorization header. The scheme word tells the server what follows: Basic means Base64 of username:password (part 4), Bearer means a token.

Advantages ​

  • Compact: small enough for an HTTP header, and quick to transmit.
  • Self-contained / stateless: the payload has what the server needs, so no database lookup.
  • Signed with HMAC (symmetric) or RSA (asymmetric), so tampering is detected.
  • Built-in expiry (exp).
  • Custom claims, such as roles.

Challenges ​

1. Invalidating a token ​

The server keeps no state, so a token stays valid until it expires. If you block a fraudulent user today but their token expires next week, how do you stop them now?

  • a) Keep a blacklist of revoked tokens (by jti) and check it on every request. This works, but brings back the database or cache lookup that JWT was supposed to avoid.
  • b) Change the signing key. Every token signed with the old key fails verification, but that logs out all users, not just the fraudulent one.
  • c) Make tokens very short-lived, for example 5 minutes. The problem then lasts at most that long.
  • d) Allow each token to be used only once. This again needs a store of used jtis.

In practice, short-lived tokens, often combined with other measures, are the most common approach.

2. Encoded, not encrypted ​

The payload is only Base64URL-encoded: anyone who has the token can decode and read it. When the content must be secret, use JWE (JSON Web Encryption), which encrypts the payload; only the holder of the key can decrypt it.

3. "alg": "none" ​

A JWT with "alg": "none" has no signature: an unsecured JWT. Anyone could create one with any payload. Such tokens must always be rejected.

4. The jwk header exploit ​

The header may carry a jwk (JSON Web Key): a public key, given by its RSA n (modulus) and e (exponent). An attacker can change the payload, sign it with their own private key and put their own public key in the jwk header. A server that verifies with that key will accept the forged token. Never verify a token with a key taken from the token itself.

5. Use kid with trusted keys ​

Instead, the header may carry a kid (key id). The issuer publishes its public keys, each with an id, at a well-known URL such as https://{auth server}/.well-known/jwks.json. The verifier takes the kid from the token, looks up the key in that trusted list, and verifies with it. Public keys are not secret, so publishing them is fine. The trust lies in where the list comes from, which is one more reason to use a well-established issuer.

Next: Part 6 implements JWT login in Spring Boot: registration, issuing tokens, validating them on every request, and refresh tokens.

Check yourself ​

  1. What is a JWT, and for which three things is it mainly used?
  2. Why is a session id stateful, and what does that cost on every request?
  3. Does a resource server need to call the authentication server to validate each JWT? Why not?
  4. Name the three parts of a JWT and what each contains.
  5. What is the difference between registered, public and private claims? Give three registered claims.
  6. How is the signature produced with HMAC, and with RSA? Which key verifies an RSA-signed token?
  7. Why should you never put a password in the payload?
  8. What does Bearer in the Authorization header mean, compared with Basic?
  9. How can you invalidate a JWT before it expires? Give the trade-off of each way.
  10. What is wrong with a token whose alg is none?
  11. Explain the jwk exploit and how kid with a JWKS URL avoids it.
Answers
  1. A signed JSON object for passing information securely; authentication, authorization and SSO.
  2. The data lives on the server, keyed by a random id; every request needs a database or cache lookup, and all servers must share the sessions.
  3. No: it verifies the signature and expiry itself with the secret or the issuer's public key.
  4. Header (type and algorithm), payload (claims), signature (over the encoded header and payload).
  5. Registered: reserved names with fixed meanings (iss, sub, exp, ...). Public: custom but meant for many parties. Private: custom, for internal use between specific parties.
  6. HMAC: signed and verified with the same secret. RSA: signed with the private key, verified with the public key.
  7. The payload is only encoded; anyone can decode it.
  8. Bearer means a token follows; Basic means Base64 of username:password.
  9. Blacklist by jti (needs a lookup again), change the key (logs everyone out), short-lived tokens (window stays open briefly), single-use tokens (needs a store).
  10. It is unsigned, so anyone can forge it; reject it.
  11. An attacker signs a forged token with their own key and puts their public key in the jwk header; verifying with it accepts the forgery. With kid, the verifier picks the key from the issuer's trusted published list instead.