Yaltopia-Ticket-Admin/dev-docs/API_STANDARDS.md

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

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.)

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 protection
  • Path=/: Cookie scope
  • Max-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/logout to 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: 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

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:

  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

  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