12 KiB
12 KiB
API Client Standards
Industry Best Practices Implemented
1. Separation of Concerns
- Public API Instance: Used for unauthenticated endpoints (login, register, forgot password)
- Authenticated API Instance: Used for protected endpoints requiring authentication
- This prevents unnecessary token attachment to public endpoints
2. Cookie-Based Authentication (Recommended)
Configuration
withCredentials: true // Enables sending/receiving cookies
This allows the backend to set httpOnly cookies which are:
- Secure: Not accessible via JavaScript (prevents XSS attacks)
- Automatic: Browser automatically sends with each request
- Industry Standard: Used by major platforms (Google, Facebook, etc.)
Backend Requirements for Cookie-Based Auth
Login Response:
POST /auth/login
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
Response Body:
{
"user": {
"id": "user-id",
"email": "user@example.com",
"role": "ADMIN"
}
}
Cookie Attributes Explained:
HttpOnly: Prevents JavaScript access (XSS protection)Secure: Only sent over HTTPS (production)SameSite=Strict: CSRF protectionPath=/: Cookie scopeMax-Age: Expiration time in seconds
Logout:
POST /auth/logout
Set-Cookie: access_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0
Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=0
Token Refresh:
POST /auth/refresh
Cookie: refresh_token=<jwt>
Response:
Set-Cookie: access_token=<new_jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
3. Fallback: localStorage (Current Implementation)
For backends that don't support httpOnly cookies, the system falls back to localStorage:
- Token stored in
localStorage.access_token - Automatically added to Authorization header
- Less secure than cookies (vulnerable to XSS)
4. Authentication Flow
Login
// Uses publicApi (no token required)
adminApiHelpers.login({ email, password })
Response Expected (Cookie-based):
{
"user": {
"id": "user-id",
"email": "user@example.com",
"role": "ADMIN",
"firstName": "John",
"lastName": "Doe"
}
}
// + Set-Cookie headers
Response Expected (localStorage fallback):
{
"access_token": "jwt-token",
"refresh_token": "refresh-token",
"user": {
"id": "user-id",
"email": "user@example.com",
"role": "ADMIN"
}
}
Logout
// Centralized logout handling
await adminApiHelpers.logout()
- Calls backend
/auth/logoutto clear httpOnly cookies - Clears localStorage (access_token, user)
- Prevents duplicate logout logic across components
Token Refresh (Automatic)
// Automatically called on 401 response
adminApiHelpers.refreshToken()
- Refreshes expired access token using refresh token
- Retries failed request with new token
- If refresh fails, logs out user
Get Current User
adminApiHelpers.getCurrentUser()
- Validates token and fetches current user data
- Useful for session validation
5. Interceptor Improvements
Request Interceptor
- Adds
withCredentials: trueto send cookies - Adds Authorization header if localStorage token exists (fallback)
- Bearer token format:
Authorization: Bearer <token>
Response Interceptor
- 401 Unauthorized:
- Attempts automatic token refresh
- Retries original request
- If refresh fails, auto-logout and redirect
- Prevents infinite loops on login page
- 403 Forbidden: Shows permission error toast
- 404 Not Found: Shows resource not found toast
- 500 Server Error: Shows server error toast
- Network Error: Shows connection error toast
6. Security Best Practices
✅ Implemented:
- Separate public/private API instances
- Cookie support with
withCredentials: true - Bearer token authentication (fallback)
- Automatic token injection
- Automatic token refresh on 401
- Centralized logout with backend call
- Auto-redirect on 401 (with login page check)
- Retry mechanism for failed requests
✅ Backend Should Implement:
- httpOnly cookies for tokens
- Secure flag (HTTPS only)
- SameSite=Strict (CSRF protection)
- Short-lived access tokens (15 min)
- Long-lived refresh tokens (7 days)
- Token rotation on refresh
- Logout endpoint to clear cookies
⚠️ Additional Production Recommendations:
- Rate limiting on login endpoint
- Account lockout after failed attempts
- Two-factor authentication (2FA)
- IP whitelisting for admin access
- Audit logging for all admin actions
- Content Security Policy (CSP) headers
- CORS configuration
- Request/response encryption for sensitive data
7. Security Comparison
| Feature | localStorage | httpOnly Cookies |
|---|---|---|
| XSS Protection | ❌ Vulnerable | ✅ Protected |
| CSRF Protection | ✅ Not vulnerable | ⚠️ Needs SameSite |
| Automatic Sending | ❌ Manual | ✅ Automatic |
| Cross-domain | ✅ Easy | ⚠️ Complex |
| Mobile Apps | ✅ Works | ❌ Limited |
| Industry Standard | ⚠️ Common | ✅ Recommended |
8. Error Handling
All API errors are consistently handled:
- User-friendly error messages
- Toast notifications for feedback
- Proper error propagation for component-level handling
- Automatic retry on token expiration
9. Type Safety
All API methods have TypeScript types for:
- Request parameters
- Request body
- Response data (can be improved with response types)
Usage Examples
Login Flow (Cookie-based)
try {
const response = await adminApiHelpers.login({ email, password })
const { user } = response.data // No access_token in response
// Verify admin role
if (user.role !== 'ADMIN') {
throw new Error('Admin access required')
}
// Store user data only (token is in httpOnly cookie)
localStorage.setItem('user', JSON.stringify(user))
// Navigate to dashboard
navigate('/admin/dashboard')
} catch (error) {
toast.error('Login failed')
}
Authenticated Request
// Token automatically sent via cookie or Authorization header
const response = await adminApiHelpers.getUsers({ page: 1, limit: 20 })
Logout
// Centralized logout (clears cookies and localStorage)
await adminApiHelpers.logout()
navigate('/login')
Automatic Token Refresh
// Happens automatically on 401 response
// No manual intervention needed
const response = await adminApiHelpers.getUsers()
// If token expired, it's automatically refreshed and request retried
API Endpoint Requirements
Authentication Endpoints
POST /auth/login
- Public endpoint (no authentication required)
- Validates credentials
- Cookie-based: Sets httpOnly cookies in response headers
- localStorage fallback: Returns access_token in response body
- Returns user data
- Should verify user role on backend
Request:
{
"email": "admin@example.com",
"password": "password123"
}
Response (Cookie-based):
HTTP/1.1 200 OK
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
{
"user": {
"id": "123",
"email": "admin@example.com",
"role": "ADMIN",
"firstName": "John",
"lastName": "Doe"
}
}
Response (localStorage fallback):
{
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"user": {
"id": "123",
"email": "admin@example.com",
"role": "ADMIN"
}
}
POST /auth/logout
- Protected endpoint
- Clears httpOnly cookies
- Invalidates tokens on server
- Clears server-side session
Response:
HTTP/1.1 200 OK
Set-Cookie: access_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0
Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=0
{
"message": "Logged out successfully"
}
POST /auth/refresh
- Protected endpoint
- Reads refresh_token from httpOnly cookie
- Returns new access token
- Implements token rotation (optional)
Request:
Cookie: refresh_token=<jwt>
Response:
HTTP/1.1 200 OK
Set-Cookie: access_token=<new_jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
Set-Cookie: refresh_token=<new_jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
{
"message": "Token refreshed"
}
GET /auth/me
- Protected endpoint
- Returns current authenticated user
- Useful for session validation
Response:
{
"id": "123",
"email": "admin@example.com",
"role": "ADMIN",
"firstName": "John",
"lastName": "Doe"
}
Backend Implementation Guide
Node.js/Express Example
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body
// Validate credentials
const user = await validateUser(email, password)
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' })
}
// Generate tokens
const accessToken = generateAccessToken(user)
const refreshToken = generateRefreshToken(user)
// Set httpOnly cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
})
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/auth/refresh',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
})
// Return user data (no tokens in body)
res.json({ user: sanitizeUser(user) })
})
// Logout endpoint
app.post('/auth/logout', (req, res) => {
res.clearCookie('access_token')
res.clearCookie('refresh_token', { path: '/auth/refresh' })
res.json({ message: 'Logged out successfully' })
})
// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token
if (!refreshToken) {
return res.status(401).json({ message: 'No refresh token' })
}
try {
const decoded = verifyRefreshToken(refreshToken)
const newAccessToken = generateAccessToken(decoded)
res.cookie('access_token', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000
})
res.json({ message: 'Token refreshed' })
} catch (error) {
res.status(401).json({ message: 'Invalid refresh token' })
}
})
// Auth middleware
const authMiddleware = (req, res, next) => {
const token = req.cookies.access_token ||
req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ message: 'No token provided' })
}
try {
const decoded = verifyAccessToken(token)
req.user = decoded
next()
} catch (error) {
res.status(401).json({ message: 'Invalid token' })
}
}
CORS Configuration
app.use(cors({
origin: 'http://localhost:5173', // Your frontend URL
credentials: true // Important: Allow cookies
}))
Migration Notes
If migrating from the old implementation:
- Login now uses
publicApiinstead ofadminApi - Added
withCredentials: truefor cookie support - Logout is centralized and calls backend endpoint
- Automatic token refresh on 401 responses
- Backend should set httpOnly cookies instead of returning tokens
- Frontend stores only user data, not tokens (if using cookies)
Testing
Test Cookie-based Auth
- Login and check browser DevTools > Application > Cookies
- Should see
access_tokenandrefresh_tokencookies - Cookies should have HttpOnly, Secure, SameSite flags
- Make authenticated request - cookie sent automatically
- Logout - cookies should be cleared
Test localStorage Fallback
- Backend returns
access_tokenin response body - Token stored in localStorage
- Token added to Authorization header automatically
- Works for backends without cookie support