Authentication is the backbone of every modern web application. Whether you're building a REST API, a mobile app, or a sprawling microservices architecture, you need a reliable way to answer one question: who is making this request, and what are they allowed to do?
Traditional session-based authentication where the server stores session state in memory or a database works fine for monolithic web apps. But it starts to creak under the weight of distributed systems. Sessions require shared storage across servers, don't scale horizontally, and don't play nicely with mobile apps or third-party API consumers.
This is where JWT (JSON Web Token) comes in. It's become the go-to standard for stateless authentication in APIs, mobile apps, and microservices.
In this article, we'll cover:
- JWT structure
- The full authentication flow
- Access tokens vs refresh tokens
- How to secure your APIs with JWT
- Common mistakes developers make (and how to avoid them)
Let's dig in.
2. What Is JWT (JSON Web Token)?
A JWT is a compact, self-contained token format used to securely transmit information between two parties as a JSON object. That information can be verified and trusted because it's digitally signed.
JWTs enable stateless authentication - the server doesn't need to store session data anywhere. All the information needed to verify a user is baked into the token itself.
It's worth separating two terms that get conflated a lot:
- Authentication - verifying who you are (e.g., logging in with a username and password)
- Authorization - determining what you're allowed to access (e.g., admin vs regular user)
JWT plays a role in both, but it's fundamentally a carrier of identity and permission claims not a magic security bullet on its own.
Here's the basic flow at a glance:
User Login
↓
Server Validates Credentials
↓
JWT Generated
↓
Client Stores Token
↓
Token Sent With API Requests
↓
Server Verifies Token
↓
Access Granted
3. Understanding JWT Structure
A JWT is made up of three parts, separated by dots:
Header.Payload.Signature
Header
The header describes the token type and the algorithm used to sign it.
{
"alg": "HS256",
"typ": "JWT"
}
Payload
The payload contains the claims - statements about the user and additional metadata.
{
"userId": "123",
"role": "admin",
"exp": 1720000000
}
Claims generally fall into three categories:
-
Registered claims - predefined, standard fields like
exp(expiration),iss(issuer), andaud(audience) - Public claims - custom claims agreed upon between parties, ideally namespaced to avoid collisions
- Private claims - custom claims meant only for a specific application or system
Signature
The signature is created by combining the encoded header, encoded payload, and a secret (or private key) and running it through the signing algorithm. This is what makes the token tamper-proof: if anyone modifies the payload without the secret, the signature won't match anymore, and the server will reject the token.
Important: JWTs are encoded, not encrypted. Anyone can decode the payload and read it. The signature only guarantees the data hasn't been tampered with it doesn't hide the data.
4. How JWT Authentication Flow Works
Here's what happens end-to-end during login and subsequent requests:
User enters credentials
↓
Backend validates username/password
↓
Server creates JWT
↓
Token returned to client
↓
Client stores token
↓
Client sends token with API requests
↓
Backend verifies token
↓
Protected resource returned
Breaking that down into concrete pieces:
- Login endpoint - accepts credentials, checks them against the database
- Token generation - on success, the server signs a JWT containing user identity and role claims
- Middleware verification - every subsequent request runs through middleware that validates the token
- Protected routes - routes that require a valid token before granting access
5. Access Tokens Explained
An access token is the JWT your client sends with every API request to prove it's authenticated.
Access tokens should be short-lived - typically 15 minutes or less. This limits the damage if a token is ever stolen.
Access Token
Expires: 15 minutes
Access tokens are used for:
- Authenticating API requests
- Authorizing access to protected resources
- Identifying the user making the request
A typical request looks like this:
GET /api/profile
Authorization: Bearer <access_token>
Why keep them short-lived?
- Reduced security risk if the token leaks
- Better control over how long a compromised token remains useful
- A smaller exposure window overall
6. Refresh Tokens Explained
Short-lived access tokens are great for security, but they create a UX problem: if tokens expire every 15 minutes, users would need to log in constantly. That's where refresh tokens come in.
A refresh token is a long-lived credential used solely to obtain a new access token it's never sent to your APIs directly.
Access Token Expired
↓
Send Refresh Token
↓
Server Validates Refresh Token
↓
Generate New Access Token
↓
Continue Session
Key considerations when implementing refresh tokens:
- Longer expiration - days or weeks, rather than minutes
- Secure storage - never expose these to client-side JavaScript
- Token rotation - issue a new refresh token every time one is used, invalidating the old one
- Revocation - maintain a way to invalidate refresh tokens (e.g., on logout or suspected compromise)
7. Access Token vs Refresh Token: Key Differences
| Feature | Access Token | Refresh Token |
|---|---|---|
| Purpose | API access | Generate new tokens |
| Lifetime | Short | Long |
| Sent frequently | Yes | No |
| Stored | Memory / secure storage | Secure storage |
| Risk | Lower | Higher |
| Used by | APIs | Authentication server |
8. Where Should JWT Tokens Be Stored?
Where you store your tokens on the client matters a lot for security.
Local Storage
Pros: Simple to implement.
Cons: Vulnerable to XSS (Cross-Site Scripting) attacks if an attacker injects malicious JavaScript, they can read anything in local storage.
Cookies
Cookies can be made much safer with the right flags:
- HttpOnly - prevents JavaScript from reading the cookie, mitigating XSS
- Secure - ensures the cookie is only sent over HTTPS
- SameSite - protects against CSRF (Cross-Site Request Forgery) by controlling when cookies are sent on cross-site requests
Mobile Applications
Mobile platforms have their own secure storage mechanisms:
- Android - Keystore
- iOS - Keychain
-
Flutter - secure storage packages (e.g.,
flutter_secure_storage)
9. Protecting APIs With JWT Middleware
On the backend, JWT verification typically happens in middleware that runs before your route handlers.
Request
↓
JWT Middleware
↓
Extract Token
↓
Verify Signature
↓
Check Expiration
↓
Attach User Data
↓
Controller Executes
In practice, this looks something like:
app.get("/profile",
authenticateToken,
profileController
);
Your middleware is responsible for:
-
Token extraction - pulling the token out of the
Authorizationheader - Signature verification - confirming the token hasn't been tampered with
- Expiration checking - rejecting expired tokens
- User identification - attaching decoded user data to the request object for downstream handlers
10. JWT Security Best Practices
Use HTTPS Always
Without HTTPS, tokens can be intercepted in transit via man-in-the-middle attacks. This isn't optional.
Keep Access Tokens Short-Lived
5–30 minutes
Validate Token Expiration (and More)
Don't just check that a token exists validate:
- The
expclaim - The
iss(issuer) - The
aud(audience)
Use Strong Signing Algorithms
Avoid weak secrets and deprecated algorithms. Use well-vetted algorithms like HS256 (with a strong, random secret) or RS256 (asymmetric signing) depending on your architecture.
Rotate Refresh Tokens
Rotating refresh tokens on every use:
- Prevents token reuse
- Helps you detect stolen tokens (if an old, rotated-out token is used again, that's a red flag)
11. Common JWT Authentication Mistakes
Mistake 1: Storing Sensitive Data Inside the JWT
Problem: The JWT payload is encoded, not encrypted anyone can decode and read it.
Never store:
- Passwords
- Payment details
- Other private/sensitive information
Solution: Only include the claims you actually need (user ID, role, expiration).
Mistake 2: Using Long-Lived Access Tokens
Problem: If an access token is stolen, the attacker has a long window of access.
Solution: Keep access tokens short-lived and rely on refresh tokens for longevity.
Mistake 3: Not Validating Tokens Properly
Problem: Simply checking that a token exists isn't enough.
Solution: Always validate:
- Signature
- Expiration
- User status (e.g., is the account still active?)
- Permissions
Mistake 4: Keeping Refresh Tokens Forever
Problem: A stolen refresh token that never expires can generate unlimited sessions indefinitely.
Solution: Implement rotation, expiration, and revocation for refresh tokens.
12. JWT Authentication in Microservices Architecture
JWT shines in distributed systems because it removes the need for shared session storage between services.
Client
↓
API Gateway
↓
Auth Service
↓
Microservices
Benefits:
- Stateless authentication across services
- No need for a centralized session store
- Easier horizontal scaling
- Enables service-to-service authentication using the same token infrastructure
Each microservice can independently verify the token's signature and check role-based permissions without calling back to a central auth server for every request.
13. JWT vs Session-Based Authentication
| Feature | JWT | Session |
|---|---|---|
| Storage | Client side | Server side |
| Scalability | High | Requires session storage |
| Server memory | No session required | Required |
| Best for | APIs / mobile / microservices | Traditional web apps |
| Revocation | More complex | Easier |
When to choose which:
- Choose JWT when you're building APIs, mobile backends, or microservices that need to scale horizontally without shared state.
- Choose session-based auth when you're building a traditional server-rendered web app where easy revocation and simplicity matter more than statelessness.
14. Implementing Role-Based Authorization With JWT
Authentication and authorization work together but answer different questions:
Authentication: User is logged in
Authorization: User can access admin features
You can embed roles directly in the token payload:
{
"userId": 123,
"role": "admin"
}
Then check that role on the backend before allowing access:
if (user.role !== "admin") {
return forbidden;
}
This pattern is common in:
- Admin dashboards
- SaaS applications
- Enterprise systems with tiered permissions
15. Testing JWT Authentication
Before shipping, run through this checklist:
Authentication
- ✓ Invalid credentials are rejected
- ✓ Tokens are generated correctly
- ✓ Expired tokens are blocked
Security
- ✓ HTTPS is enabled everywhere
- ✓ Tokens are stored securely on the client
- ✓ Refresh tokens are protected from exposure
Authorization
- ✓ Users cannot access other users' data
- ✓ Roles are properly validated on every protected route
16. Final Thoughts
JWT authentication offers a scalable, stateless way to secure modern applications especially REST APIs, mobile apps, SaaS platforms, and microservices.
But a secure implementation takes more than just generating a token and calling it done. You need to carefully think through:
- Token lifecycle (access vs refresh)
- Storage strategy (cookies vs local storage vs secure device storage)
- Expiration handling
- Refresh and rotation mechanisms
- API-level authorization checks
JWT is a powerful tool but like any tool, its security depends entirely on how well it's implemented.
Top comments (0)