Back to Blog

API Security Best Practices: Authentication, Authorization

API security best practices in 2026 — JWT authentication, OAuth 2.0, rate limiting, input validation, OWASP API Security Top 10, and production-grade TypeScript

Viprasol Tech Team
14 min read
Updated 2026

API Security Best Practices: Authentication, Authorization, and OWASP Top 10

Quick answer. APIs are the most attacked surface in modern applications, involved in 83% of web application breaches per Verizon's 2024 DBIR, yet most exploits are preventable with standard techniques. The core stack is authentication (JWT, sessions, or API keys), authorization design, input validation, rate limiting, and mitigating the OWASP API Security Top 10.

APIs are the most attacked surface in modern applications. The 2024 Verizon Data Breach Investigations Report found that APIs were involved in 83% of web application breaches. Most of those breaches exploited issues that are entirely preventable with standard techniques.

This guide covers the full API security stack: authentication patterns, authorization design, input validation, rate limiting, and the OWASP API Security Top 10 with practical mitigation code for each.


Authentication: JWT vs. Sessions vs. API Keys

JWT (JSON Web Tokens)

JWTs are signed tokens that contain claims. The server doesn't need to store session state — the token is self-contained.

import jwt from 'jsonwebtoken';
import { z } from 'zod';

const JWT_SECRET = process.env.JWT_SECRET!; // Must be ≥32 bytes, rotated regularly
const ACCESS_TOKEN_TTL = '15m';  // Short-lived — this is critical
const REFRESH_TOKEN_TTL = '7d';

interface TokenPayload {
  sub: string;       // User ID
  email: string;
  role: string;
  iat: number;       // Issued at
  exp: number;       // Expiry
}

// Issue token pair on login
function issueTokens(userId: string, email: string, role: string) {
  const accessToken = jwt.sign(
    { sub: userId, email, role },
    JWT_SECRET,
    { expiresIn: ACCESS_TOKEN_TTL, algorithm: 'HS256' }
  );

  const refreshToken = jwt.sign(
    { sub: userId, type: 'refresh' },
    JWT_SECRET,
    { expiresIn: REFRESH_TOKEN_TTL, algorithm: 'HS256' }
  );

  return { accessToken, refreshToken };
}

// Verify and decode
function verifyAccessToken(token: string): TokenPayload {
  try {
    return jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }) as TokenPayload;
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      throw new Error('TOKEN_EXPIRED');
    }
    throw new Error('TOKEN_INVALID');
  }
}

// Auth middleware
export const authenticate = async (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid Authorization header' });
  }

  const token = authHeader.slice(7);
  try {
    const payload = verifyAccessToken(token);
    req.user = payload;
    next();
  } catch (err: any) {
    const status = err.message === 'TOKEN_EXPIRED' ? 401 : 401;
    res.status(status).json({ error: err.message });
  }
};

JWT security rules:

  • Access tokens expire in 15 minutes — not hours, not days
  • Refresh tokens expire in 7 days and are stored in an HTTP-only cookie (not localStorage)
  • Use HS256 (HMAC) or RS256 (RSA) — never none
  • Rotate the JWT secret periodically; have a rotation procedure

API Key Authentication

For machine-to-machine and partner integrations:

import crypto from 'crypto';

// Generate API key
function generateApiKey(): { key: string; hash: string } {
  const key = `vip_${crypto.randomBytes(32).toString('hex')}`;
  const hash = crypto.createHash('sha256').update(key).digest('hex');
  return { key, hash }; // Store hash in DB, return key once to user
}

// Verify API key (hash comparison — no plain-text storage)
async function verifyApiKey(providedKey: string): Promise<ApiClient | null> {
  const hash = crypto.createHash('sha256').update(providedKey).digest('hex');
  
  const client = await db('api_keys')
    .where({ key_hash: hash, revoked: false })
    .first();

  if (!client) return null;

  // Update last used (async — don't block the response)
  db('api_keys').where({ id: client.id }).update({ last_used_at: new Date() }).catch(console.error);

  return client;
}

Authorization: RBAC and Resource-Level Checks

Authentication answers "who are you?" Authorization answers "what can you do?"

Role-Based Access Control (RBAC)

type Permission =
  | 'users:read'
  | 'users:write'
  | 'billing:read'
  | 'billing:write'
  | 'admin:all';

const ROLE_PERMISSIONS: Record<string, Permission[]> = {
  viewer: ['users:read'],
  member: ['users:read', 'users:write'],
  billing_admin: ['users:read', 'billing:read', 'billing:write'],
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write', 'admin:all'],
};

function can(userRole: string, permission: Permission): boolean {
  return ROLE_PERMISSIONS[userRole]?.includes(permission) ?? false;
}

// Authorization middleware factory
function requirePermission(permission: Permission) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) return res.status(401).json({ error: 'Unauthenticated' });
    if (!can(req.user.role, permission)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// Usage
router.get('/admin/users', authenticate, requirePermission('admin:all'), listUsers);
router.post('/users/:id', authenticate, requirePermission('users:write'), updateUser);

Resource-Level Authorization (CRITICAL — Prevents BOLA)

Always verify the requesting user owns or has access to the specific resource:

// ❌ BAD: Only checks authentication, not ownership
app.get('/orders/:orderId', authenticate, async (req, res) => {
  const order = await db('orders').where({ id: req.params.orderId }).first();
  res.json(order); // Any authenticated user can see any order!
});

// ✅ GOOD: Verify resource ownership
app.get('/orders/:orderId', authenticate, async (req, res) => {
  const order = await db('orders')
    .where({ 
      id: req.params.orderId,
      user_id: req.user.sub,  // Scope to authenticated user's orders
    })
    .first();

  if (!order) {
    // Return 404, not 403 — don't reveal whether the resource exists
    return res.status(404).json({ error: 'Order not found' });
  }

  res.json(order);
});

This is OWASP API1:2023 — Broken Object Level Authorization (BOLA). It's the #1 API vulnerability in production systems.


🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

OWASP API Security Top 10 (2023) Mitigations

API1: Broken Object Level Authorization (BOLA)

Always scope queries by user_id (or equivalent). Never trust the ID in the URL alone.

API2: Broken Authentication

// Brute-force protection on login
import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 5,                     // 5 attempts per IP
  message: { error: 'Too many login attempts, try again in 15 minutes' },
  standardHeaders: true,
  legacyHeaders: false,
  // Additional: lock account after N failures (track in DB)
});

app.post('/auth/login', loginLimiter, handleLogin);

API3: Broken Object Property Level Authorization

Return only the fields the user should see. Never return entire DB rows:

// ❌ BAD: Returns password_hash, internal flags, admin fields
const user = await db('users').where({ id: userId }).first();
res.json(user);

// ✅ GOOD: Explicit field selection
const user = await db('users')
  .select('id', 'email', 'display_name', 'created_at', 'plan')
  .where({ id: userId })
  .first();
res.json(user);

API4: Unrestricted Resource Consumption

// Rate limiting per API key / user (not just per IP)
const apiRateLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute window
  max: 100,
  keyGenerator: (req) => req.user?.sub ?? req.ip,  // Rate limit per user, not IP
  handler: (req, res) => {
    res.status(429).json({
      error: 'Rate limit exceeded',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
    });
  },
});

// Also limit request body size
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true, limit: '1mb' }));

API5: Broken Function Level Authorization

Separate admin endpoints into a different router with explicit admin-only middleware:

// Admin router — every route requires admin role
const adminRouter = express.Router();
adminRouter.use(authenticate);
adminRouter.use(requirePermission('admin:all'));

adminRouter.get('/users', listAllUsers);
adminRouter.delete('/users/:id', deleteUser);
adminRouter.post('/users/:id/ban', banUser);

// Regular router — user-scoped endpoints
app.use('/api/v1', userRouter);
app.use('/api/v1/admin', adminRouter);  // Separate prefix

API8: Security Misconfiguration

// Security headers — use helmet
import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: true,
  hsts: { maxAge: 31536000, includeSubDomains: true },
  noSniff: true,
  xssFilter: true,
}));

// CORS — explicit whitelist, never wildcard for authenticated APIs
import cors from 'cors';

const allowedOrigins = [
  'https://app.yourproduct.com',
  'https://yourproduct.com',
  ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS: Origin not allowed'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}));

API10: Unsafe Consumption of APIs

When consuming external APIs, validate responses:

import { z } from 'zod';

const ExternalUserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string().max(255),
  // Don't pass unknown fields through to your system
});

async function fetchExternalUser(id: string): Promise<ExternalUser> {
  const response = await fetch(`https://partner-api.com/users/${id}`, {
    headers: { Authorization: `Bearer ${process.env.PARTNER_API_KEY}` },
    signal: AbortSignal.timeout(5000),  // 5-second timeout
  });

  if (!response.ok) {
    throw new Error(`Partner API error: ${response.status}`);
  }

  const raw = await response.json();
  return ExternalUserSchema.parse(raw);  // Validates AND strips unknown fields
}

Input Validation

Every external input is untrusted. Validate at the API boundary:

import { z } from 'zod';

const CreateOrderSchema = z.object({
  productId: z.string().uuid(),
  quantity: z.number().int().min(1).max(1000),
  shippingAddress: z.object({
    line1: z.string().min(1).max(200),
    city: z.string().min(1).max(100),
    country: z.string().length(2),  // ISO country code
    postalCode: z.string().regex(/^[A-Z0-9\s-]{3,10}$/i),
  }),
  couponCode: z.string().regex(/^[A-Z0-9]{6,12}$/).optional(),
});

app.post('/orders', authenticate, async (req, res) => {
  const result = CreateOrderSchema.safeParse(req.body);
  
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.flatten(),
    });
  }

  const order = await orderService.create(req.user.sub, result.data);
  res.status(201).json(order);
});

security - API Security Best Practices: Authentication, Authorization

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Logging and Monitoring

// Log security events for audit and incident response
async function logSecurityEvent(event: {
  type: 'login_failed' | 'access_denied' | 'rate_limit_hit' | 'suspicious_request';
  userId?: string;
  ip: string;
  details: Record<string, unknown>;
}): Promise<void> {
  await db('security_events').insert({
    ...event,
    occurred_at: new Date(),
  });

  // Alert on high-frequency suspicious activity
  if (event.type === 'login_failed') {
    const recentFailures = await db('security_events')
      .where({ type: 'login_failed', ip: event.ip })
      .where('occurred_at', '>', new Date(Date.now() - 60000))
      .count('id as count');

    if (Number(recentFailures[0].count) > 10) {
      await alertService.send(`Brute force attempt from IP ${event.ip}`);
    }
  }
}

Security Implementation Costs

TaskInvestment
API security audit$5,000–$15,000
Auth system implementation (JWT + refresh)$5,000–$12,000
RBAC + resource-level authorization$8,000–$20,000
Rate limiting + DDoS protection$3,000–$8,000
Full API security hardening$20,000–$50,000
Penetration test (external)$5,000–$20,000

How Viprasol Helps

We build secure APIs from the ground up — or audit and harden existing ones. Our security implementations are OWASP-aligned and include authentication, authorization, input validation, rate limiting, and monitoring.

API security review →
API Development Services →
Web Development Services →


Related Topics


securityAPIauthenticationJWTOAuthOWASP
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.