API security is not an afterthought—it is a foundational requirement that must be designed into every layer of your application. A single authentication bypass or authorization flaw can expose sensitive data, enable account takeovers, or compromise entire systems. This guide covers the authentication and authorization patterns we implement at Nexis Limited for production REST APIs.
Authentication Patterns
JWT (JSON Web Tokens)
JWTs are self-contained tokens that encode user identity and claims in a signed payload. The server issues a JWT upon successful login, and the client includes it in the Authorization header of subsequent requests. The server validates the signature without querying a database, making JWTs stateless and scalable.
import jwt from 'jsonwebtoken';
// Issue a token
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: '15m', issuer: 'nexisltd.com' }
);
const refreshToken = jwt.sign(
{ sub: user.id, tokenVersion: user.tokenVersion },
process.env.REFRESH_SECRET!,
{ expiresIn: '7d', issuer: 'nexisltd.com' }
);
return { accessToken, refreshToken };
}
// Middleware to verify token
function authenticate(req, res, next) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
try {
const payload = jwt.verify(
header.slice(7),
process.env.JWT_SECRET!,
{ issuer: 'nexisltd.com' }
);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
Use short-lived access tokens (15 minutes) paired with longer-lived refresh tokens (7 days). Store refresh tokens securely—in an HttpOnly, Secure, SameSite cookie for browser clients, or in secure storage for mobile clients. Never store JWTs in localStorage, as they are vulnerable to XSS attacks.
OAuth 2.0
OAuth 2.0 is the standard for delegated authorization. It allows users to grant third-party applications limited access to their resources without sharing credentials. The Authorization Code flow with PKCE (Proof Key for Code Exchange) is the recommended flow for both server-rendered and single-page applications. Use established libraries like Passport.js or Auth.js rather than implementing OAuth flows from scratch.
API Keys
API keys are simple to implement but offer limited security. They are best suited for server-to-server communication where the key can be stored securely. Never embed API keys in client-side code. Scope keys to specific operations and IP ranges, and rotate them regularly. Hash API keys before storing them in your database, just like passwords.
Authorization Patterns
Role-Based Access Control (RBAC)
RBAC assigns permissions through roles. A user is assigned one or more roles, and each role grants a set of permissions. This model is straightforward and works well for applications with a fixed set of user types:
const PERMISSIONS = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read']
};
function authorize(...requiredPermissions: string[]) {
return (req, res, next) => {
const userPermissions = PERMISSIONS[req.user.role] || [];
const hasPermission = requiredPermissions.every(
p => userPermissions.includes(p)
);
if (!hasPermission) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.delete('/api/users/:id', authenticate, authorize('manage_users'), deleteUser);
Attribute-Based Access Control (ABAC)
ABAC evaluates access based on attributes of the user, the resource, and the environment. For example, "a user can edit a document if they are the owner or a member of the document's organization and the document is not archived." ABAC is more expressive than RBAC and handles complex authorization rules elegantly, but it requires a policy evaluation engine.
Rate Limiting
Rate limiting protects your API from abuse, brute-force attacks, and accidental denial of service. Implement rate limiting at multiple levels:
- Global rate limit: Cap total requests per IP across all endpoints (e.g., 1,000 requests per minute).
- Endpoint-specific limits: Sensitive endpoints like login and password reset should have stricter limits (e.g., 5 attempts per minute).
- User-based limits: Authenticated users get higher limits than anonymous users.
Use a sliding window algorithm backed by Redis for distributed rate limiting. Return standard headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) so clients can self-regulate.
Additional Security Measures
- Input validation: Validate and sanitize all input using schema validation libraries like Zod or Joi. Never trust client data.
- CORS configuration: Restrict allowed origins to known domains. Avoid wildcard (
*) origins in production. - HTTPS everywhere: Enforce TLS for all API traffic. Use HSTS headers to prevent downgrade attacks.
- Request signing: For critical operations, require request signing with HMAC to ensure integrity and authenticity.
- Audit logging: Log all authentication events, authorization failures, and sensitive operations for forensic analysis.
Security is a continuous process, not a one-time implementation. Regular penetration testing, dependency auditing, and security reviews are essential to maintaining a robust API. If you need help securing your APIs or implementing identity infrastructure, contact our engineering team to discuss your requirements.