This content originally appeared on Level Up Coding - Medium and was authored by Tara Prasad Routray
Learn how to bulletproof your React & Angular apps — no more token theft, XSS attacks, or security nightmares!

Are you storing JWTs in localStorage or sessionStorage? Bad news — your app is a prime target for XSS attacks and token theft. Many developers unknowingly leave their applications vulnerable by choosing the wrong JWT storage strategy. But don’t worry — I’m about to show you the right way to handle authentication securely.
In this article, you’ll learn how to implement a bulletproof authentication system using React, Angular, Node.js, and Express while keeping security risks at bay. Let’s dive in!
The Security Risks of Storing JWTs in LocalStorage
Most developers store their authentication tokens in localStorage or sessionStorage, but this comes with serious security risks:
- XSS Attacks — Malicious scripts can steal tokens directly from localStorage.
- Token Theft — If an attacker gains access to the token, they can impersonate users.
- Manual Expiry Management — You have to manually handle token expiration and refresh.
What’s the Alternative?
HTTP-only Cookies — These cookies are automatically sent with every request, cannot be accessed, and reduce XSS risks significantly.
How HTTP-Only Cookies Work for JWT Authentication
HTTP-only cookies solve security issues by preventing JavaScript from accessing stored tokens. Instead of sending the JWT in the client’s localStorage, we store a refresh token inside a secure HTTP-only cookie. Here’s how it works:
- User logs in, then Backend generates Access Token + Refresh Token.
- Access Token is sent in the response body and stored in frontend app’s in-memory (not localStorage).
- Refresh Token is stored inside an HTTP-only, secure cookie.
- When the Access Token expires, the client sends a request to the server with the Refresh Token (from the HTTP-only cookie) to get a new Access Token.
- The process repeats, ensuring smooth authentication without exposing sensitive tokens to attackers.
Setting Up Secure Authentication in React, Angular, and Node.js
1. Backend (Node.js + Express) — JWT with HTTP-Only Cookies
We’ll set up authentication using JWT and HTTP-only cookies to enhance security.
1.1 Install Required Packages
Run the following command to install necessary dependencies:
npm install express jsonwebtoken cookie-parser cors dotenv
1.2 Backend Code (server.js)
Initialize Express App
require("dotenv").config();
const express = require("express");
const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const app = express();
app.use(express.json());
app.use(cookieParser());
// Allow frontend (React/Angular) to send credentials
app.use(cors({ origin: "http://localhost:3000", credentials: true }));
const users = [{ id: 1, username: "admin", password: "hashed_password" }];
JWT Token Generation
const generateAccessToken = (user) =>
jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "15m" });
const generateRefreshToken = (user) =>
jwt.sign({ id: user.id, username: user.username }, process.env.JWT_REFRESH_SECRET, { expiresIn: "7d" });
2. Authentication Routes
2.1 Login Route (Set Refresh Token in HTTP-Only Cookie)
app.post("/login", (req, res) => {
const { username, password } = req.body;
// Validate password (Use bcrypt/argon2 in real apps)
const user = users.find((u) => u.username === username);
if (!user || password !== "hashed_password") {
return res.status(401).json({ message: "Invalid credentials" });
}
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "Strict",
path: "/refresh-token",
});
res.json({ accessToken });
});
2.2 Refresh Token Route (Get New Access Token)
app.post("/refresh-token", (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ message: "No refresh token" });
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: "Invalid refresh token" });
const newAccessToken = generateAccessToken({ id: user.id, username: user.username });
res.json({ accessToken: newAccessToken });
});
});
2.3 Logout Route (Clear Refresh Token Cookie)
app.post("/logout", (req, res) => {
res.clearCookie("refreshToken", { path: "/refresh-token" });
res.json({ message: "Logged out" });
});
3. Start the Server
app.listen(8000, () => console.log("Server running on port 8000"));
Summary
- Login: Generates access & refresh tokens, stores refresh token in HTTP-only cookies.
- Refresh Token: Issues a new access token when the old one expires.
- Logout: Clears the refresh token from cookies.
This setup ensures secure JWT authentication while preventing XSS and token theft.
Why This Works Better Than LocalStorage
- No XSS Risks — HTTP-only cookies are not accessible via XSS attack.
- Automatic Token Refresh — No need to manually store or send refresh tokens.
- Less Code for Developers — The browser handles cookies automatically.
Handling JWT Authentication on the Frontend
1. Storing & Sending the Access Token
Since the refresh token is stored in an HTTP-only cookie, the access token should be stored in memory (not in localStorage or sessionStorage, to prevent XSS attacks).
React (Using Axios)
Login and store the access token
import axios from "axios";
const login = async (username, password) => {
try {
const res = await axios.post("http://localhost:8000/login", { username, password }, { withCredentials: true });
// Store access token in memory (React state or a global store like Redux)
const accessToken = res.data.accessToken;
console.log("Access Token:", accessToken);
return accessToken;
} catch (error) {
console.error("Login failed:", error.response.data.message);
}
};
Send the access token in API requests
const fetchUserData = async () => {
// Get the accessToken from the state of your application.
const accessToken = '...';
try {
const res = await axios.get("http://localhost:8000/user", {
headers: { Authorization: `Bearer ${accessToken}` },
});
console.log("User Data:", res.data);
} catch (error) {
console.error("Error fetching data:", error.response.data.message);
}
};
Angular (Using HttpClient)
Login and store the access token
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private accessToken: string | null = null;
constructor(private http: HttpClient) {}
login(username: string, password: string) {
return this.http.post<{ accessToken: string }>('http://localhost:8000/login', { username, password }, { withCredentials: true })
.subscribe(res => {
this.accessToken = res.accessToken;
console.log('Access Token:', this.accessToken);
});
}
getAccessToken() {
return this.accessToken;
}
}
Send the access token in API requests
getUserData() {
return this.http.get('http://localhost:8000/user', {
headers: { Authorization: `Bearer ${this.getAccessToken()}` }
});
}
2. Refreshing the Access Token Automatically
When the access token expires, request a new one using the refresh token (sent automatically via the HTTP-only cookie).
React (Axios Interceptor)
Intercept failed API calls and refresh the token automatically.
const api = axios.create({ baseURL: "http://localhost:8000", withCredentials: true });
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response.status === 403) {
try {
// Request new access token
const res = await axios.post("http://localhost:8000/refresh-token", {}, { withCredentials: true });
// Retry the failed request with new token
error.config.headers.Authorization = `Bearer ${res.data.accessToken}`;
return axios(error.config);
} catch (refreshError) {
console.error("Session expired, please log in again.");
}
}
return Promise.reject(error);
}
);
Angular (Interceptor for Token Refresh)
Use an HttpInterceptor to refresh the token when needed.
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, switchMap } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private http: HttpClient) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${this.authService.getAccessToken()}` }
});
return next.handle(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 403) {
// Refresh token and retry failed request
return this.http.post<{ accessToken: string }>('http://localhost:8000/refresh-token', {}, { withCredentials: true })
.pipe(
switchMap((res) => {
this.authService.accessToken = res.accessToken;
const newAuthReq = req.clone({ setHeaders: { Authorization: `Bearer ${res.accessToken}` } });
return next.handle(newAuthReq);
})
);
}
return throwError(() => error);
})
);
}
}
Summary
- Login: Send username & password → Receive access token & set refresh token (HTTP-only cookie)
- API Requests: Attach access token in the Authorization header
- Token Expiration Handling: If 403 Forbidden, request a new access token using the refresh token.
- Store access token in-memory (avoid localStorage/sessionStorage for security)
Conclusion: Keep Your Apps Secure!
You’ve learned how to secure JWT authentication using HTTP-only cookies, protecting against XSS and token theft.
Next Steps:
- Add Role-Based Authentication (RBAC).
- Use Rate Limiting to block brute-force attacks.
- Store refresh tokens in cookies, keep access tokens in-memory (state stores).
If you enjoyed reading this article and have found it useful, then please give it a clap, share it with your friends, and follow me to get more updates on my upcoming articles. You can connect with me on LinkedIn. Or, you can visit my official website: tararoutray.com to know more about me.
STOP Getting Hacked! The Ultimate JWT Authentication Guide for Devs was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Tara Prasad Routray

Tara Prasad Routray | Sciencx (2025-03-07T01:02:05+00:00) STOP Getting Hacked! The Ultimate JWT Authentication Guide for Devs. Retrieved from https://www.scien.cx/2025/03/07/stop-getting-hacked-the-ultimate-jwt-authentication-guide-for-devs/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.