48 Storing and Validating JWT Tokens
48 Storing and Validating JWT Tokens
JSON Web Token (JWT) has become the de facto standard for modern authentication, especially for web and mobile applications. Using JWT allows us to build a stateless authentication system, so the server no longer needs to store user sessions in the backend. Even so, there are still many fundamental questions among engineers, particularly about where to store the JWT token and how to validate it securely. This article will thoroughly cover those two key aspects: storing and validating JWTs, complete with code examples, a simple architectural simulation, and several best practices worth following.
What Is a JWT?
Put simply, a JWT is a specially formatted string made up of three main parts:
- Header
- Payload
- Signature
A JWT is sent to the client after the user successfully logs in, and is then sent back to the server on every request that requires authorization. JWTs are not only efficient but also portable—they can be carried anywhere, anytime.
Example JWT token:
1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiJKb2huIERvZSJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkPart 1: Storing the JWT Token on the Client
Storing a JWT token on the client side can be a double-edged sword. Choosing the wrong storage location can open up security holes such as XSS and CSRF. There are several main options:
| Method | Pros | Cons |
|---|---|---|
| Local Storage | Easy to implement, broadly accessible | Vulnerable to XSS |
| Session Storage | Like localStorage, but session-based | Vulnerable to XSS, lost when the tab is closed |
| Cookie (HTTPOnly) | Not accessible from JS, safe from XSS | Vulnerable to CSRF without SameSite |
Table 1: Comparison of JWT token storage locations
Simulating JWT Storage in Local Storage
1// After a successful login
2localStorage.setItem('access_token', jwtToken);
3
4// Retrieving the token on subsequent requests
5const token = localStorage.getItem('access_token');
6fetch('/api/resource', {
7 headers: {
8 'Authorization': `Bearer ${token}`
9 }
10});However, localStorage is highly vulnerable to XSS attacks. If an attacker manages to inject malicious scripts into the application, the token can be stolen with ease.
Storage with an HTTPOnly Cookie
The best approach for higher security needs is to store the JWT in a cookie marked with the HttpOnly and SameSite flags.
1// The server sends a set-cookie header on login
2Set-Cookie: token=eyJhbGc...; HttpOnly; Secure; SameSite=StrictThis way, the token cannot be accessed from JavaScript, so an XSS attack cannot retrieve it.
Pro Tips:
- Use
SameSite=Strictif your application does not need to share cookies across domains. - Add
Secureso the cookie is only sent over HTTPS.
Part 2: Checking/Validating the JWT Token on the Server
Once the JWT token is stored on the client, the next step is to validate every incoming request. Typically, this process is handled by middleware in the backend. Let’s go through it on two stacks: Node.js (Express) and Go (Golang).
2.1 Checking a JWT Token in Node.js/Express
To verify a JWT in Express, we’ll use the jsonwebtoken
library.
Express Middleware: authenticateJWT.js
1const jwt = require('jsonwebtoken');
2
3function authenticateJWT(req, res, next) {
4 const authHeader = req.headers.authorization;
5
6 if (authHeader) {
7 // Format: "Bearer <token>"
8 const token = authHeader.split(' ')[1];
9
10 jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
11 if (err) {
12 return res.sendStatus(403); // Forbidden
13 }
14 req.user = user;
15 next();
16 });
17 } else {
18 res.sendStatus(401); // Unauthorized
19 }
20}
21
22module.exports = authenticateJWT;Using the Middleware
1const express = require('express');
2const authenticateJWT = require('./authenticateJWT');
3
4const app = express();
5
6app.get('/api/resource', authenticateJWT, (req, res) => {
7 res.json({ message: "Hello, " + req.user.name });
8});2.2 Verifying a JWT in Golang
In Golang, we typically use the github.com/golang-jwt/jwt/v4
package.
1import (
2 "github.com/golang-jwt/jwt/v4"
3 "net/http"
4 "strings"
5)
6
7var secretKey = []byte("your-secret-key")
8
9func authMiddleware(next http.Handler) http.Handler {
10 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11 authHeader := r.Header.Get("Authorization")
12 if authHeader == "" {
13 http.Error(w, "missing auth header", http.StatusUnauthorized)
14 return
15 }
16
17 tokenString := strings.TrimPrefix(authHeader, "Bearer ")
18 token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
19 return secretKey, nil
20 })
21
22 if err != nil || !token.Valid {
23 http.Error(w, "invalid token", http.StatusForbidden)
24 return
25 }
26 next.ServeHTTP(w, r)
27 })
28}Flow Diagram: The JWT Validation Process
Let’s visually illustrate the flow of storing and checking a JWT token.
sequenceDiagram
participant Client
participant Server
Client->>Server: Login (username, password)
Server-->>Client: JWT Token (response body / Set-Cookie)
Note over Client: Simpan token (localStorage/cookie)
Client->>Server: Request API (Authorization: Bearer)
Server->>Server: Verifikasi JWT (cek signature & expired)
alt Valid
Server-->>Client: Data/Respon Success
else Tidak Valid
Server-->>Client: 401/403 Error
end
Case Study: Client Local Storage vs. Cookies
| Scenario | localStorage | Cookies (HTTPOnly) |
|---|---|---|
| Can be stolen via XSS | Yes | No |
| Can be used for CSRF | No (generally) | Yes (without SameSite=Strict) |
| Accessible from JavaScript | Yes | No |
| Safari/Android implementation | Consistent | Sometimes problematic with CORS and domain/subdomain |
Best Practices
- Use an HTTPOnly cookie if your application places a high priority on security.
- Add a separate refresh token mechanism, rather than baking it into the main JWT.
- Keep the JWT library on the server up to date.
- Always validate the exp (expiration) field in the JWT payload.
- Do not store sensitive data in the payload.
- Implement revocation by keeping a blacklist on the server (optional, if you need session revocation).
Conclusion
Storing and validating JWT tokens is not merely a technical implementation, but also an architectural decision that greatly affects your application’s security.
Before choosing a storage method, understand the trade-off between convenience (developer experience) and safety (security risk). For small-to-medium scale applications, localStorage is often the practical choice, but for financial applications or sensitive data, a cookie with HttpOnly is an absolute must.
Always reassess your application’s security landscape periodically, and don’t hesitate to adopt the latest best practices in the world of JWT authentication. Happy coding!