477 lines
12 KiB
Markdown
477 lines
12 KiB
Markdown
# 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
|
|
```typescript
|
|
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:**
|
|
```http
|
|
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 protection
|
|
- `Path=/`: Cookie scope
|
|
- `Max-Age`: Expiration time in seconds
|
|
|
|
**Logout:**
|
|
```http
|
|
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:**
|
|
```http
|
|
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
|
|
```typescript
|
|
// Uses publicApi (no token required)
|
|
adminApiHelpers.login({ email, password })
|
|
```
|
|
|
|
**Response Expected (Cookie-based):**
|
|
```json
|
|
{
|
|
"user": {
|
|
"id": "user-id",
|
|
"email": "user@example.com",
|
|
"role": "ADMIN",
|
|
"firstName": "John",
|
|
"lastName": "Doe"
|
|
}
|
|
}
|
|
// + Set-Cookie headers
|
|
```
|
|
|
|
**Response Expected (localStorage fallback):**
|
|
```json
|
|
{
|
|
"access_token": "jwt-token",
|
|
"refresh_token": "refresh-token",
|
|
"user": {
|
|
"id": "user-id",
|
|
"email": "user@example.com",
|
|
"role": "ADMIN"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Logout
|
|
```typescript
|
|
// Centralized logout handling
|
|
await adminApiHelpers.logout()
|
|
```
|
|
- Calls backend `/auth/logout` to clear httpOnly cookies
|
|
- Clears localStorage (access_token, user)
|
|
- Prevents duplicate logout logic across components
|
|
|
|
#### Token Refresh (Automatic)
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
adminApiHelpers.getCurrentUser()
|
|
```
|
|
- Validates token and fetches current user data
|
|
- Useful for session validation
|
|
|
|
### 5. Interceptor Improvements
|
|
|
|
#### Request Interceptor
|
|
- Adds `withCredentials: true` to 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)
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// Token automatically sent via cookie or Authorization header
|
|
const response = await adminApiHelpers.getUsers({ page: 1, limit: 20 })
|
|
```
|
|
|
|
### Logout
|
|
```typescript
|
|
// Centralized logout (clears cookies and localStorage)
|
|
await adminApiHelpers.logout()
|
|
navigate('/login')
|
|
```
|
|
|
|
### Automatic Token Refresh
|
|
```typescript
|
|
// 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:**
|
|
```json
|
|
{
|
|
"email": "admin@example.com",
|
|
"password": "password123"
|
|
}
|
|
```
|
|
|
|
**Response (Cookie-based):**
|
|
```http
|
|
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):**
|
|
```json
|
|
{
|
|
"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
|
|
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:**
|
|
```http
|
|
Cookie: refresh_token=<jwt>
|
|
```
|
|
|
|
**Response:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"id": "123",
|
|
"email": "admin@example.com",
|
|
"role": "ADMIN",
|
|
"firstName": "John",
|
|
"lastName": "Doe"
|
|
}
|
|
```
|
|
|
|
## Backend Implementation Guide
|
|
|
|
### Node.js/Express Example
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
app.use(cors({
|
|
origin: 'http://localhost:5173', // Your frontend URL
|
|
credentials: true // Important: Allow cookies
|
|
}))
|
|
```
|
|
|
|
## Migration Notes
|
|
|
|
If migrating from the old implementation:
|
|
1. Login now uses `publicApi` instead of `adminApi`
|
|
2. Added `withCredentials: true` for cookie support
|
|
3. Logout is centralized and calls backend endpoint
|
|
4. Automatic token refresh on 401 responses
|
|
5. Backend should set httpOnly cookies instead of returning tokens
|
|
6. Frontend stores only user data, not tokens (if using cookies)
|
|
|
|
## Testing
|
|
|
|
### Test Cookie-based Auth
|
|
1. Login and check browser DevTools > Application > Cookies
|
|
2. Should see `access_token` and `refresh_token` cookies
|
|
3. Cookies should have HttpOnly, Secure, SameSite flags
|
|
4. Make authenticated request - cookie sent automatically
|
|
5. Logout - cookies should be cleared
|
|
|
|
### Test localStorage Fallback
|
|
1. Backend returns `access_token` in response body
|
|
2. Token stored in localStorage
|
|
3. Token added to Authorization header automatically
|
|
4. Works for backends without cookie support
|