What is a JWT?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format defined by RFC 7519. It allows two parties to exchange claims — small pieces of information — in a way that can be verified and trusted. JWTs are the backbone of modern authentication systems across the web.
When you log into a web application that uses JWT-based authentication, the server creates a token containing information about you (like your user ID and role), signs it with a secret key, and sends it back to your browser. On every subsequent request, your browser sends that token in an HTTP header, and the server verifies the signature to confirm the token is authentic and unmodified.
The key advantage of JWTs over traditional session cookies is that the server does not need to store session data. All the information the server needs is encoded right inside the token itself. This makes JWTs particularly well-suited for stateless APIs, microservice architectures, and single sign-on systems where sharing session state between servers would be complex.
The Three Parts: Header, Payload, Signature
Every JWT consists of exactly three parts, separated by dots (.). Here is an example token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Each of those three sections — separated by the dots — has a specific purpose:
Header: The first part is a Base64url-encoded JSON object that describes the token type and the signing algorithm being used. Decoding the header from the example above gives:
{
"alg": "HS256",
"typ": "JWT"
}
The alg field specifies the algorithm (HS256 means HMAC with SHA-256), and typ confirms this is a JWT.
Payload: The second part is also a Base64url-encoded JSON object. It contains the claims — the actual data you want to transmit. Decoding the payload gives:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Claims can be anything you need: a user ID, an email address, permission roles, or an expiration timestamp. The JWT specification defines a set of standard claims (covered below), but you can also add any custom claims your application requires.
Signature: The third part is the cryptographic signature that ensures the token has not been tampered with. For HS256, the signature is computed as:
HMAC-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Only someone who knows the secret key can produce a valid signature. If an attacker modifies even a single character in the header or payload, the signature will no longer match, and the server will reject the token.
Standard Claims
The JWT specification defines several registered claims. These are not mandatory, but they are widely used and recognized by most JWT libraries:
exp(Expiration Time): A Unix timestamp indicating when the token expires. After this time, the token should be considered invalid. This is the most important security claim.iat(Issued At): A Unix timestamp indicating when the token was created. Useful for determining the token's age.nbf(Not Before): A Unix timestamp before which the token must not be accepted. This allows you to issue tokens in advance that activate at a future time.sub(Subject): A string identifying the subject of the token, usually a user ID. This is the primary identifier for who the token represents.iss(Issuer): A string identifying who issued the token. In a multi-service environment, this helps the verifier know which service created the token.aud(Audience): A string or array identifying the intended recipients of the token. A token meant for your API should be rejected by a different API that checks this claim.
How JWT Signing Works
JWT supports multiple signing algorithms, but the two most common are HS256 (symmetric) and RS256 (asymmetric).
HS256 (HMAC + SHA-256) uses a single shared secret key for both signing and verification. The server that creates the token and the server that verifies it must both know the same secret. This is simple to implement and works well when the same application handles both token creation and verification.
// Signing with HS256 (conceptual)
signature = HMAC_SHA256(header_b64 + "." + payload_b64, shared_secret)
RS256 (RSA + SHA-256) uses a public/private key pair. The token is signed with the private key, and anyone can verify it using the corresponding public key. This is ideal for distributed systems where multiple services need to verify tokens but only one service (the auth server) should be able to create them.
// Signing with RS256 (conceptual)
signature = RSA_SHA256(header_b64 + "." + payload_b64, private_key)
// Verification uses the public key
valid = RSA_SHA256_VERIFY(header_b64 + "." + payload_b64, signature, public_key)
The choice between HS256 and RS256 depends on your architecture. For a single monolithic application, HS256 is simpler. For microservices or scenarios where third parties need to verify tokens, RS256 is the better choice.
How to Verify a JWT
When your server receives a JWT, it must verify the token before trusting its contents. Proper verification involves several steps:
- Split the token into its three dot-separated parts: header, payload, and signature.
- Decode the header to determine the signing algorithm.
- Recompute the signature using the header, payload, and the secret or public key.
- Compare signatures: If the computed signature matches the signature in the token, the token has not been tampered with.
- Check the
expclaim: Reject the token if it has expired. - Check the
issandaudclaims: Ensure the token was issued by a trusted issuer and is intended for your application.
In practice, you should always use a well-tested JWT library for your language rather than implementing verification yourself. Libraries like jsonwebtoken for Node.js, PyJWT for Python, or java-jwt for Java handle all of these steps and protect against known attack vectors.
Common JWT Security Mistakes
JWTs are secure by design, but incorrect usage introduces vulnerabilities. Here are the most common mistakes developers make:
- Storing sensitive data in the payload: The payload is only encoded, not encrypted. Anyone can decode it. Never put passwords, credit card numbers, or other secrets in a JWT.
- Not validating the signature: Some applications decode the payload without verifying the signature first. This means an attacker can forge any token they want.
- Using the "none" algorithm: The JWT spec allows
"alg": "none"for unsigned tokens. If your server accepts this, attackers can create tokens without a valid signature. Always reject tokens with the "none" algorithm in production. - Using weak or default secrets: An HS256 token signed with the key "secret" or "password123" can be brute-forced in seconds. Use a long, random secret (at least 256 bits).
- Setting excessively long expiration times: A token that lasts 30 days gives an attacker 30 days to exploit a stolen token. Keep access tokens short-lived (5 to 30 minutes) and use refresh tokens for longer sessions.
- Storing JWTs in localStorage: Tokens in localStorage are vulnerable to cross-site scripting (XSS) attacks. Prefer HttpOnly cookies, which cannot be accessed by JavaScript.
JWT vs Session Tokens
JWTs and traditional server-side sessions are both valid approaches to authentication, and each has tradeoffs.
Server-side sessions store a session ID in a cookie. The actual session data (user info, permissions) lives on the server, typically in a database or Redis cache. The session ID is just an opaque reference. This means the server can revoke a session instantly by deleting its record, and no sensitive data is exposed to the client.
JWTs are self-contained. The token carries all the data the server needs, so there is no server-side state to manage. This simplifies horizontal scaling because any server can verify the token independently. However, revoking a JWT before its expiration is difficult — since there is no server-side record to delete, you would need to maintain a token blocklist, which partially defeats the purpose of going stateless.
Use JWTs when you need stateless authentication across multiple services, APIs, or microservices. Use server-side sessions when you need fine-grained control over session lifetime and the ability to revoke sessions instantly, such as in applications where security is paramount (banking, healthcare).
Many modern applications use a hybrid approach: short-lived JWTs for API access tokens combined with server-side refresh token storage for session management. This gives you the scalability benefits of JWTs while maintaining the ability to revoke access when needed.