Skip to content

REST API Authentication

Complete guide to authenticating REST API requests using JWT tokens and development mode.

The REST API supports two authentication methods:

  1. Production Mode: JWT tokens via Authorization header (default)
  2. Development Mode: User ID and permissions via headers (local only)

All authenticated requests require a JWT token:

GET /api/users/user-123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Endpoint: POST /api/auth/authenticate

POST /api/auth/authenticate
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!"
}

Response:

{
"success": true,
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"user": {
"id": "user-123",
"email": "user@example.com",
"permissions": ["users:read", "orders:read"]
}
}

Include in all subsequent requests:

GET /api/users/user-123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT tokens contain:

{
"sub": "user-123",
"email": "user@example.com",
"name": "John Doe",
"permissions": [
"users:read",
"users:create",
"orders:read"
],
"iat": 1700056800,
"exp": 1700060400
}

Claims:

  • sub - User ID (subject)
  • email - User’s email
  • name - Display name
  • permissions - Array of permission strings
  • iat - Issued at timestamp
  • exp - Expiration timestamp
  • Token expires after 1 hour (3600 seconds)
  • Refresh before expiration for uninterrupted access
  • Use refresh token (valid for 7 days)

Endpoint: POST /api/auth/refresh-token

POST /api/auth/refresh-token
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Response:

{
"success": true,
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600
}
class ApiClient {
private accessToken: string | null = null;
private refreshToken: string | null = null;
async request(url: string, options: RequestInit = {}) {
// Add authorization header
const headers = {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
};
let response = await fetch(url, { ...options, headers });
// Handle token expiration
if (response.status === 401) {
await this.refreshAccessToken();
// Retry with new token
headers.Authorization = `Bearer ${this.accessToken}`;
response = await fetch(url, { ...options, headers });
}
return response;
}
private async refreshAccessToken() {
const response = await fetch('/api/auth/refresh-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
if (!response.ok) {
// Refresh token expired, need to re-authenticate
this.redirectToLogin();
return;
}
const data = await response.json();
this.accessToken = data.accessToken;
}
async login(email: string, password: string) {
const response = await fetch('/api/auth/authenticate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
}
return data;
}
async logout() {
if (this.accessToken) {
await fetch('/api/auth/revoke-token', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: this.accessToken })
});
}
this.accessToken = null;
this.refreshToken = null;
}
}

CRITICAL SECURITY WARNING

Development mode (DEVELOPMENT_AUTH_ENABLED=true) BYPASSES ALL AUTHENTICATION.

  • ❌ NO JWT validation
  • ❌ NO token verification
  • ❌ NO permission enforcement
  • ⚠️ Anyone can impersonate ANY user with ANY permissions

NEVER enable in production. NEVER commit .env with this enabled.

Environment variable:

Terminal window
DEVELOPMENT_AUTH_ENABLED=true
GET /api/users/user-123
X-Dev-User-Id: dev-user-123
X-Dev-Permissions: users:read,users:create,orders:read

Headers:

  • X-Dev-User-Id - User ID to impersonate
  • X-Dev-Permissions - Comma-separated list of permissions
POST /api/users
X-Dev-User-Id: dev-admin
X-Dev-Permissions: users:create,users:read,admin:all
Content-Type: application/json
{
"email": "newuser@example.com",
"password": "password123",
"profile": {
"firstName": "Test",
"lastName": "User"
}
}
const response = await fetch('http://localhost:3003/api/users', {
method: 'POST',
headers: {
'X-Dev-User-Id': 'dev-admin',
'X-Dev-Permissions': 'users:create,users:read',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'newuser@example.com',
password: 'password123',
profile: {
firstName: 'Test',
lastName: 'User'
}
})
});
const data = await response.json();
console.log(data);
Authorization: Bearer <jwt-token>
Content-Type: application/json
X-Dev-User-Id: <user-id>
X-Dev-Permissions: <comma-separated-permissions>
Content-Type: application/json
X-Correlation-Id: <custom-trace-id>
Accept: application/json
Accept-Language: en-US

The API Gateway includes these headers in responses:

X-Correlation-Id: abc-123-def-456
X-Request-Id: req-789-ghi-012
Content-Type: application/json
Access-Control-Allow-Origin: *

The API Gateway automatically handles CORS:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Dev-User-Id, X-Dev-Permissions
Access-Control-Max-Age: 86400
OPTIONS /api/users
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

Response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Dev-User-Id, X-Dev-Permissions
Access-Control-Max-Age: 86400
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "Unauthorized",
"message": "Invalid or expired token",
"code": "INVALID_TOKEN"
}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "Unauthorized",
"message": "Token has expired",
"code": "TOKEN_EXPIRED"
}
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "Forbidden",
"message": "Missing required permission: users:create",
"code": "INSUFFICIENT_PERMISSIONS",
"requiredPermission": "users:create"
}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "Unauthorized",
"message": "No authorization header provided",
"code": "NO_AUTH_HEADER"
}

DO:

  • Store tokens in httpOnly cookies (server-side)
  • Use secure session storage
  • Clear tokens on logout

DON’T:

  • Store tokens in localStorage (XSS risk)
  • Store tokens in sessionStorage (XSS risk)
  • Log tokens in console or logs

DO:

  • Use HTTPS in production
  • Use WSS for WebSocket connections
  • Rotate JWT secrets periodically

DON’T:

  • Use HTTP in production
  • Share tokens across domains
  • Transmit tokens in URLs

DO:

  • Use for local development only
  • Document required permissions
  • Disable before deployment

DON’T:

  • Enable in production
  • Commit DEVELOPMENT_AUTH_ENABLED=true
  • Use in CI/CD pipelines
  • Skip security testing