How JWT Works
JWT (JSON Web Token) is a Web standard that wraps information in a JSON object and uses a digital signature to ensure its credibility. Its main features are stateless and self-contained.
- Stateless: The server does not need to store any user information.
- Self-contained: JWT itself contains all the information needed to verify user identity (such as user ID, permissions, etc). After the server receives the JWT, it can directly parse these details without querying a database or cache.
These two features are very important. Let's explain them with a real example.
Session-Cookie Authentication Process
Take web service as an example. Let's look at the traditional cookie-based authentication:
When a user logs in successfully, the server generates a sessionID, for example, abcd1234
.
At the same time, the server stores a mapping from sessionID to user information in a database (usually a cache database like Redis):
abcd1234: {
user_id: "test-user",
permissions: ["read"]
}
The server sends this sessionID to the browser through the Set-Cookie
response header. After that, every time the browser requests the server, it includes this sessionID in the request header:
Cookie: sessionID=abcd1234
With this setup, the server reads the sessionID from the request header and verifies the user's identity by checking the database.
Note that this authentication method is stateful because the server depends on a database to store the mapping between sessionID and user info.
Also, this method is not self-contained, because the request header only contains the sessionID, which is just a random string and does not have any user information.
Here is a sequence diagram of the Session-Cookie authentication process:
sequenceDiagram
participant Browser
participant Server
participant Database as Session Store
Browser->>Server: POST /login (username, password)
Server->>Server: Verify credentials
Server->>Server: Create session
Server->>Database: Store session info (sessionID -> userInfo)
Server-->>Browser: Response with Set-Cookie: sessionID=...
Browser->>Server: GET /api/data (with sessionID cookie)
Server->>Database: Look up sessionID
Database-->>Server: Return userInfo
Server->>Server: Process request with userInfo
Server-->>Browser: Response with requested data
JWT Authentication Process
Let's think about how to achieve stateless and self-contained authentication.
Maybe we can put the user information directly into the request header, like this:
Cookie: userInfo={"user_id":"test-user","permissions":["read"]}
This method is stateless. The server can get user info from the request header, and does not need to rely on other services. It is also self-contained because all user info is inside the request.
But there is a big problem: How can the server trust the data from the client?
The client can fake the userInfo field, for example:
Cookie: userInfo={"user_id":"admin-user","permissions":["read","write","delete"]}
Anyone can change the request header and claim to be an admin, and then do anything on the server.
So, we need a safer way to be sure the data from the client is trusted. This is the problem that JWT solves.
The JWT authentication process works like this:
When a user logs in successfully, the server will check the user's authentication info (like user_id, permissions, etc.), sign it with a private key, and generate a JWT token to send back to the client.
For example, this JWT stores the information {"user_id":"test-user","permissions":["read"]}
:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGVzdC11c2VyIiwicGVybWlzc2lvbnMiOlsicmVhZCJdLCJpc3MiOiJsYWJ1bGFkb25nLm9ubGluZSIsImlhdCI6MTc1ODk3NDU3OCwiZXhwIjoxNzU5MTQ3Mzc4fQ.lZln8NiYZACSKVLSCzSTB8_VnDUp4WJHiuzO0CUrSw_QagnNTqyGppRG8HoFsRqnpjaxNNTEoqIgwVl6ib0kUO-m9JMnIj4cQKIpZXGYPP8cO-PmvvbWCWr8yqMv_481lS2_XgyXMbo4ZjmpZIca-MSxLETY1wQcLrrzS_r75oukItpmnjAnePtD-cp0bcgRVmrCW3DQrxA6FQw1WSM2Fwz9MvYDGxBu8D8s0aBDqlPceK0W7IC2J-hktcxX5FK9qr76GeYRAFC71DH05e68cxqOCwSxJ-JJLE5uzA-AIAXy9gReY4lkzreFEl2LtwsFg7zM3Tv39CUHgxuitd0mog
In every future request, the client will send this JWT token in the Authorization
field of the HTTP request header:
Authorization: Bearer <JWT Token>
After the server receives the JWT token, it can directly parse the user's authentication info, then use the public key to verify the signature. If the verification passes, the information is trusted. Otherwise, the data may be tampered with.
Here is a sequence diagram of the JWT authentication process:
sequenceDiagram
participant Client as Browser/Client
participant Server
Client->>Server: POST /login (username, password)
Server->>Server: Verify credentials
Server->>Server: Create JWT with user info payload
Server->>Server: Sign JWT with private key
Server-->>Client: Response with JWT
Client->>Server: GET /api/data (Authorization: Bearer <JWT>)
Server->>Server: Verify JWT signature with public key
Server->>Server: Extract user info from payload
Server->>Server: Process request with userInfo
Server-->>Client: Response with requested data
Structure of JWT
Let's look at the above JWT example. A JWT has three parts, separated by dots (.
):
<Header>.<Payload>.<Signature>

Each part is Base64 encoded. You can decode it to get a plain JSON object.
JWT is not encrypted
JWT looks like a random string, but it is just Base64 encoded. Anyone can decode it to see the content. So do not put any sensitive info in the JWT.
JWT is not designed to protect data by encryption. It uses digital signature to guarantee the data is complete and trusted.
Header
The Header is a JSON string describing metadata about the JWT. It usually has two fields: token type (typ
) and signature algorithm (alg
).
For example, if you decode the JWT header with Base64, you get:
$ echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
{
"alg": "RS256",
"typ": "JWT"
}
alg
: Signature algorithm. For example,RS256
means using the RSA private key for signing.typ
: Token type. Fixed asJWT
.
Payload
The Payload is also a JSON string. It stores the actual data you want to transfer.
The JWT standard defines some official fields:
jti
: JWT IDiss
: Issuersub
: Subjectaud
: Audienceiat
: Issued Atnbf
: Not Beforeexp
: Expiration Time
These fields are optional. Their use will be explained later in the OAuth chapter.
You can also add your own custom fields. For example, decoding the payload of the above JWT gives:
$ echo "eyJ1c2VyX2lkIjoidGVzdC11c2VyIiwicGVybWlzc2lvbnMiOlsicmVhZCJdLCJpc3MiOiJsYWJ1bGFkb25nLm9ubGluZSIsImlhdCI6MTc1ODk3NDU3OCwiZXhwIjoxNzU5MTQ3Mzc4fQ" | base64 -d
{
"user_id": "test-user",
"permissions": ["read"],
"iss": "labuladong.online",
"iat": 1758974578,
"exp": 1759147378
}
You can see that the payload stores the user_id
and permissions
fields. The iss
is the issuer (labuladong.online
). iat
is the issued time, exp
is the expiration time. If the issuer does not match or the token is expired, the JWT is invalid.
Signature
The Signature is the core part of JWT. It ensures the integrity and authenticity of the JWT.
For example, if the signing algorithm is RS256
, you need to use your RSA private key to sign the Base64 encoded <Header>.<Payload>
string:
# Data to sign: <Header>.<Payload>
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGVzdC11c2VyIiwicGVybWlzc2lvbnMiOlsicmVhZCJdLCJpc3MiOiJsYWJ1bGFkb25nLm9ubGluZSIsImlhdCI6MTc1ODk3NDU3OCwiZXhwIjoxNzU5MTQ3Mzc4fQ
After signing, add the Base64 encoded signature to the end. This forms the complete <Header>.<Payload>.<Signature>
JWT token.
Verify JWT Token
When the server receives this JWT token, it can use the matching public key to check if the JWT has been tampered with.
You can verify this JWT on the jwt.io website.
Paste the JWT I provided on the left side:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGVzdC11c2VyIiwicGVybWlzc2lvbnMiOlsicmVhZCJdLCJpc3MiOiJsYWJ1bGFkb25nLm9ubGluZSIsImlhdCI6MTc1ODk3NDU3OCwiZXhwIjoxNzU5MTQ3Mzc4fQ.lZln8NiYZACSKVLSCzSTB8_VnDUp4WJHiuzO0CUrSw_QagnNTqyGppRG8HoFsRqnpjaxNNTEoqIgwVl6ib0kUO-m9JMnIj4cQKIpZXGYPP8cO-PmvvbWCWr8yqMv_481lS2_XgyXMbo4ZjmpZIca-MSxLETY1wQcLrrzS_r75oukItpmnjAnePtD-cp0bcgRVmrCW3DQrxA6FQw1WSM2Fwz9MvYDGxBu8D8s0aBDqlPceK0W7IC2J-hktcxX5FK9qr76GeYRAFC71DH05e68cxqOCwSxJ-JJLE5uzA-AIAXy9gReY4lkzreFEl2LtwsFg7zM3Tv39CUHgxuitd0mog
Paste the public key I provided on the lower right:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqiVXbVKq8oaFSn7ZWFCn
FkRY3W7MUoxlSG6ONImhzh6pofe5o6SoVNJdSHwNinJaATpMz3mNW7jKq2+ySLv1
fFXNlZyKjQaT47l6LmeiKNpxoH6dmvjUofTS0Jz98jMuz0hR9yaEqKAU46wr9Fty
Q4TwmEanpRajt62zNY2CbUHHsGO9wGrfY0xOijDMg2JTriX4G66VIzanYq/fcpC+
5OmY8p8ZgPovoOcDnUOjzotnln5JDGwx53K/4NvwzX2nsdHBb2ydgkZDCbuIC9ys
ccmAUNXSCCiStxt/05UyOW4s561IQd1ajTl+oa7FGHgj7sPummRwJhj8PAjhIQRL
kwIDAQAB
-----END PUBLIC KEY-----
You will see Valid JWT
and Signature Verified
, which means this JWT token is valid. The fields inside, like user_id
and permissions
, are trustworthy.

If you change any data in the JWT payload, the verification will fail.