smaple.tr
API security

API Security Best Practices: OWASP Top 10, mTLS, and BOLA Prevention [2026]

Mehmet Kurtipek
July 28, 2026
12 min read
API security
OWASP API Top 10
JWT
mtls
input validation
authentication

OWASP's API Security Top 10 catalogues the vulnerabilities that cause most real-world API breaches. They are not exotic zero-days — they are implementation patterns that appear repeatedly across poorly secured APIs: insufficient authorization checks, weak token management, missing input validation, and misconfigured access controls. Understanding and systematically addressing each one is the foundation of API security best practices.

This guide covers the complete API security implementation stack: OWASP API Security Top 10 defenses, OAuth 2.0 and JWT implementation with RS256, refresh token rotation, mTLS for microservice communication, input validation, API key management, security headers, and deployment checklists. By the end, you will have a concrete reference for implementing security at every layer of your API.

API Security Best Practices: OWASP Top 10 Vulnerability Catalog

OWASP's API Security project maintains a ranked list of the most critical API vulnerabilities based on real-world incident data. Each represents a pattern of failure that appears across organizations and technology stacks.

# Vulnerability Impact Defense
1 Broken Object Level Authorization (BOLA) Critical Per-resource ownership checks
2 Broken Authentication Critical OAuth 2.0 + JWT RS256
3 Broken Object Property Level Authorization High Response field filtering
4 Unrestricted Resource Consumption High Rate limiting + quotas
5 Broken Function Level Authorization High Scope-based access control
6 Unrestricted Access to Sensitive Business Flows Medium Flow validation
7 Server-Side Request Forgery (SSRF) High URL allowlisting
8 Security Misconfiguration Medium Infrastructure as Code
9 Improper Inventory Management Medium API versioning + deprecation
10 Unsafe API Consumption Medium Input validation on external data

BOLA: The Most Common API Vulnerability

Broken Object Level Authorization (BOLA) occurs when an API returns data based on an ID parameter without verifying that the requesting user is authorized to access that specific object. The attack is trivially simple: change the ID in the request URL from your own resource ID to another user's.

// Vulnerable: trusts the ID in the URL without checking ownership
router.get('/orders/:orderId', async (req, res) => {
  const order = await Order.findById(req.params.orderId);
  return res.json(order); // Returns any order to any authenticated user
});

// Secure: verifies ownership before returning data
router.get('/orders/:orderId', jwtValidation, async (req, res) => {
  const order = await Order.findOne({
    id: req.params.orderId,
    customerId: req.auth.sub, // JWT subject must match order owner
  });

  if (!order) {
    // Return 404, not 403 — avoid leaking that the resource exists
    return res.status(404).json({ error: 'Not found' });
  }

  return res.json(order);
});

BOLA defense requires that every object-returning endpoint performs an ownership or authorization check before returning data. This is distinct from authentication (is the token valid?) and function-level authorization (can this user call this endpoint type?). All three checks are required independently.

Broken Object Property Level Authorization

This vulnerability involves returning or accepting more fields than the caller is authorized to see or modify. An API that returns password_hash, internal_credit_score, or admin_notes to regular users exposes sensitive data even if the user cannot modify those fields.

// Define separate response types per authorization level
const customerPublicFields = ['id', 'name', 'email', 'createdAt'];
const customerAdminFields = [...customerPublicFields, 'internalScore', 'adminNotes'];

router.get('/customers/:id', jwtValidation, async (req, res) => {
  const customer = await Customer.findById(req.params.id);
  if (!customer) return res.status(404).json({ error: 'Not found' });

  const isAdmin = req.auth.roles?.includes('admin');
  const fields = isAdmin ? customerAdminFields : customerPublicFields;
  const response = pick(customer, fields);

  return res.json(response);
});

Explicit field allowlisting is more secure than blocklisting. As the data model evolves, new fields are invisible to unauthorized callers by default rather than requiring a blocklist update.

OAuth 2.0 Implementation for API Security

OAuth 2.0 is the authorization foundation. The grant type selection depends on the client type:

Authorization Code Flow with PKCE: web applications and mobile apps. The user authenticates with the authorization server directly; the application receives a code that it exchanges for tokens. PKCE prevents code interception attacks, which is especially important for mobile apps where client secrets cannot be securely stored.

Client Credentials Flow: server-to-server communication. No user involvement — services authenticate to each other using a client ID and secret to obtain access tokens.

Device Flow: IoT devices and CLI tools with limited or no browser capability. The device displays a code; the user authenticates on a secondary device.

The key principle: the API never receives user credentials. Credentials go only to the authorization server. The API receives only tokens.

Scope Definition and Enforcement

Scopes define the granularity of access. Well-designed scopes follow the principle of least privilege:

// Scope definitions
const SCOPES = {
  ORDERS_READ: 'orders:read',
  ORDERS_WRITE: 'orders:write',
  CUSTOMERS_READ: 'customers:read',
  ADMIN_ALL: 'admin:*',
} as const;

// Scope enforcement middleware
function requireScopes(requiredScopes: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const tokenScopes = (req.auth?.scope || '').split(' ');
    const hasAllScopes = requiredScopes.every(
      scope => tokenScopes.includes(scope) || tokenScopes.includes('admin:*')
    );

    if (!hasAllScopes) {
      return res.status(403).json({
        error: 'insufficient_scope',
        required: requiredScopes,
        provided: tokenScopes,
      });
    }
    next();
  };
}

// Applied to routes
router.get('/orders', jwtValidation, requireScopes([SCOPES.ORDERS_READ]), listOrders);
router.post('/orders', jwtValidation, requireScopes([SCOPES.ORDERS_WRITE]), createOrder);

JWT Security: RS256 and Token Lifecycle

JWT security depends on correct implementation of signing, validation, and lifecycle management.

RS256 Signing

RS256 uses an RSA private key to sign tokens and a public key to verify them. Only the authorization server holds the private key. Any API service can verify tokens using the public key from the JWKS (JSON Web Key Set) endpoint:

import * as jose from 'jose';

// Authorization server: sign token with private key
async function createAccessToken(userId: string, scopes: string[]): Promise<string> {
  const privateKey = await jose.importPKCS8(process.env.JWT_PRIVATE_KEY!, 'RS256');

  return new jose.SignJWT({
    sub: userId,
    scope: scopes.join(' '),
    jti: crypto.randomUUID(), // Unique token ID for revocation
  })
    .setProtectedHeader({ alg: 'RS256', kid: 'key-2026-01' })
    .setIssuer('https://auth.yourcompany.com')
    .setAudience('https://api.yourcompany.com')
    .setIssuedAt()
    .setExpirationTime('15m')
    .sign(privateKey);
}

// API service: verify token with public key from JWKS
const JWKS = jose.createRemoteJWKSet(
  new URL('https://auth.yourcompany.com/.well-known/jwks.json'),
  { cacheMaxAge: 600_000 } // Cache JWKS for 10 minutes
);

async function validateToken(token: string) {
  const { payload } = await jose.jwtVerify(token, JWKS, {
    issuer: 'https://auth.yourcompany.com',
    audience: 'https://api.yourcompany.com',
    algorithms: ['RS256'],
  });
  return payload;
}

Refresh Token Rotation

Every use of a refresh token should issue a new refresh token and invalidate the previous one. This rotation pattern provides two benefits: the refresh token window is limited to the interval between uses, and token theft is detectable (when the attacker uses the stolen token, the legitimate user's next refresh will fail because the token is already used):

async function refreshTokens(refreshToken: string): Promise<TokenPair> {
  // Find and validate the refresh token
  const stored = await RefreshToken.findOne({
    token: hashToken(refreshToken),
    isRevoked: false,
    expiresAt: { $gt: new Date() },
  });

  if (!stored) {
    // Token not found or revoked — possible replay attack
    // Revoke all refresh tokens for this user as a precaution
    if (stored?.userId) {
      await RefreshToken.updateMany(
        { userId: stored.userId },
        { isRevoked: true }
      );
    }
    throw new UnauthorizedError('Invalid refresh token');
  }

  // Revoke used token (rotation)
  await RefreshToken.updateOne({ _id: stored._id }, { isRevoked: true });

  // Issue new token pair
  const newAccessToken = await createAccessToken(stored.userId, stored.scopes);
  const newRefreshToken = await createRefreshToken(stored.userId, stored.scopes);

  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

mTLS for Service-to-Service Security

Mutual TLS (mTLS) extends standard TLS by requiring both client and server to present valid certificates. In a microservice architecture, this provides cryptographic identity verification for every inter-service connection — not just encryption.

Without mTLS, a compromised service inside the cluster can make requests to other services without additional authentication. With mTLS, every service must prove its identity through a certificate issued by the cluster's internal Certificate Authority.

mTLS Implementation with Istio

Service mesh (Istio, Linkerd) implements mTLS transparently at the infrastructure level, without changes to application code:

# Istio PeerAuthentication: enforce mTLS for all services in namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # Reject any non-mTLS traffic

---
# AuthorizationPolicy: service A can only call specific services
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: order-service-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: order-service
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/api-gateway
              - cluster.local/ns/production/sa/customer-service
      to:
        - operation:
            methods: ["GET", "POST"]

With this configuration, the order service accepts connections only from the API gateway and the customer service, verified by certificate identity. No other service can reach it, regardless of what claims are in the request headers.

Input Validation

Input validation is the defense against injection attacks (SQL, NoSQL, command injection), format-based exploits, and unexpected behavior from malformed data. It should happen before any business logic executes.

import { z } from 'zod';

const createOrderSchema = z.object({
  customerId: z.string().uuid('Invalid customer ID format'),
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().min(1).max(100),
  })).min(1, 'Order must have at least one item').max(50, 'Maximum 50 items per order'),
  shippingAddress: z.object({
    line1: z.string().min(1).max(200),
    city: z.string().min(1).max(100),
    postalCode: z.string().regex(/^[A-Z0-9]{3,10}$/, 'Invalid postal code'),
    countryCode: z.string().length(2).toUpperCase(),
  }),
  notes: z.string().max(500).optional(),
});

router.post('/orders', jwtValidation, async (req, res) => {
  const result = createOrderSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message,
      })),
    });
  }

  const order = await createOrder(result.data);
  return res.status(201).json(order);
});

Validation libraries (Zod for TypeScript, Joi for JavaScript, Pydantic for Python) provide both runtime validation and static type inference from the same schema definition.

API Key Management and Secrets

API keys are appropriate for third-party developer integrations and machine-to-machine scenarios where OAuth 2.0 would be disproportionate. Key management requires specific practices:

import crypto from 'crypto';

// Generate and store API keys
async function generateApiKey(userId: string, name: string): Promise<{
  key: string;
  keyId: string;
}> {
  const rawKey = crypto.randomBytes(32).toString('hex');
  const keyId = crypto.randomBytes(8).toString('hex');
  const fullKey = `sk_${keyId}_${rawKey}`; // Prefixed for easy identification

  // Store only the hash — the raw key is shown once and never stored
  const keyHash = crypto.createHash('sha256').update(fullKey).digest('hex');

  await ApiKey.create({
    keyId,
    keyHash,
    userId,
    name,
    createdAt: new Date(),
    expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days
    lastUsedAt: null,
  });

  return { key: fullKey, keyId };
}

// Validate API key on request
async function validateApiKey(rawKey: string): Promise<ApiKey | null> {
  const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');

  const apiKey = await ApiKey.findOne({
    keyHash,
    isRevoked: false,
    expiresAt: { $gt: new Date() },
  });

  if (apiKey) {
    await ApiKey.updateOne({ _id: apiKey._id }, { lastUsedAt: new Date() });
  }

  return apiKey;
}

Secret management in production: secrets (database passwords, JWT private keys, third-party API keys) belong in a secrets manager (HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager), not in environment variables committed to source control. Applications retrieve secrets at startup from the secrets manager, with rotation handled automatically.

Security Headers

Security headers instruct browsers on how to handle your API responses, closing several classes of attack that HTTP alone does not prevent:

import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'none'"],
      scriptSrc: ["'self'"],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"],
    },
  },
  strictTransportSecurity: {
    maxAge: 31536000,   // 1 year
    includeSubDomains: true,
    preload: true,
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

// Additional API-specific headers
app.use((req, res, next) => {
  res.set({
    'Cache-Control': 'no-store',       // Prevent sensitive data caching
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'Permissions-Policy': 'geolocation=(), camera=(), microphone=()',
  });
  next();
});

Production Security Checklist

Before Every API Deployment

  • OWASP BOLA check: every object endpoint verifies ownership (ownerId === req.auth.sub)
  • JWT signed with RS256, not HS256
  • Access token lifetime ≤15 minutes
  • Refresh token rotation implemented and tested
  • Token revocation endpoint tested
  • Scope enforcement tested for each authorization tier
  • Input validation rejects malformed payloads with 400
  • Rate limiting verified at 429 threshold
  • CORS origin allowlist tested (wildcard blocked in production)
  • Security headers present in all responses
  • mTLS enforced for internal service communication
  • No secrets in source code or environment variable files
  • API key hashing verified (not stored in plaintext)
  • SSRF prevention: outbound URL validation against allowlist

Ongoing

  • Dependency audit: npm audit or pip-audit in CI pipeline
  • JWKS key rotation: schedule quarterly key rotation
  • API key expiration: 90-day maximum, rotation enforced
  • Penetration testing: annual external API security assessment
  • Incident response plan: documented procedure for token compromise scenario

Conclusion

API security best practices are not a checklist completed once — they are a set of patterns applied consistently across every endpoint, every service, and every deployment. BOLA prevention requires discipline at every object-returning endpoint. JWT RS256 with token rotation requires a correct key management infrastructure. mTLS requires service mesh or application-level certificate management. Input validation requires schema definitions maintained in parallel with business logic.

The patterns described here address the OWASP API Security Top 10 systematically. None are novel — they represent the current state of practice for production API security. The organizations that implement them consistently are the ones that avoid breach incidents. The organizations that implement them selectively are the ones that appear in breach reports.

Related Articles

August 11, 2026

MLOps Guide: Taking Machine Learning Models to Production [2026]

87% of machine learning models built by data science teams never reach production. The models work — they pass cross-validation, they score well on holdout sets, they demonstrate genuine predictive value. The problem is not the modeling. The problem is everything that happens between a notebook experiment and a reliable, monitored, production system. MLOps is the discipline that closes that gap. This guide covers the full MLOps stack: maturity levels, tooling choices (MLflow, DVC, Kubeflow

Read More: MLOps Guide: Taking Machine Learning Models to Production [2026]
August 10, 2026

LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]

General-purpose LLMs are impressive. They can write code, summarize documents, answer questions, and translate between languages with reasonable accuracy. But "reasonable" is not good enough when your application requires consistent output format, domain-specific terminology, a particular tone, or behavior that the base model was never trained to exhibit. That gap is where fine-tuning matters. Fine-tuning updates a model's weights on your specific data, changing how the model behaves — not

Read More: LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]
August 9, 2026

Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]

Computer vision has moved well past the research phase. The models are trained, the frameworks are mature, the hardware is accessible, and the use cases are generating measurable returns. What was a specialized capability requiring deep expertise in 2018 is now deployable infrastructure — if you know which component to reach for and where the real complexity lives. This guide covers computer vision applications across industrial, medical, logistics, and document processing domains. It expl

Read More: Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]