API Authentication & Security: Complete Implementation Guide

by API Status Check Team

API Authentication & Security: Complete Implementation Guide

Securing your API isn't optional—it's the foundation of trust between you and your users. A single authentication vulnerability can expose user data, enable unauthorized access, and destroy years of reputation building.

This guide covers production-ready authentication patterns used by Auth0, AWS Cognito, Okta, and Stripe. You'll learn when to use each method, how to implement them securely, and common pitfalls to avoid.

Table of Contents

  1. Authentication vs Authorization
  2. Authentication Methods Comparison
  3. API Keys
  4. JWT (JSON Web Tokens)
  5. OAuth 2.0
  6. Security Best Practices
  7. Implementation Patterns
  8. Common Security Vulnerabilities
  9. Testing & Monitoring
  10. Production Checklist

Authentication vs Authorization

Before diving into implementation, understand the difference:

  • Authentication = "Who are you?" (proving identity)
  • Authorization = "What can you do?" (checking permissions)

Example flow:

// 1. Authentication: Verify the user is who they claim to be
const user = await verifyAuthToken(request.headers.authorization);

// 2. Authorization: Check if they can perform this action
if (!user.permissions.includes('write:posts')) {
  throw new ForbiddenError('Insufficient permissions');
}

// 3. Proceed with the request
await createPost(user.id, request.body);

Authentication Methods Comparison

Method Best For Security Level Complexity Revocation
API Keys Server-to-server, simple auth Medium Low Easy
JWT Stateless auth, mobile/SPA apps High Medium Hard*
OAuth 2.0 Third-party integrations High High Easy
Basic Auth Internal tools, prototypes Low Very Low N/A

*JWT revocation requires additional infrastructure (deny lists, short expiry + refresh tokens)

When to Use Each Method

Use API Keys when:

  • Server-to-server communication (backend → API)
  • Simple authentication needs
  • You control both client and server
  • Example: Stripe API, SendGrid

Use JWT when:

  • Building single-page apps (SPAs) or mobile apps
  • You need stateless authentication
  • Session data needs to be stored in the token
  • Example: Internal application auth

Use OAuth 2.0 when:

  • Third-party apps need access to your API
  • "Login with Google/GitHub" flows
  • Granular permission scopes required
  • Example: GitHub API, Slack API

API Keys

What Are API Keys?

API keys are unique identifiers assigned to API consumers. They're the simplest authentication method.

Stripe API key example:

curl https://api.stripe.com/v1/charges \
  -u sk_test_4eC39HqLyjWDarjtT1zdp7dc:

Implementing API Keys

1. Generate Secure Keys

import crypto from 'crypto';

function generateApiKey(): string {
  // Generate 32 random bytes, convert to hex (64 characters)
  return crypto.randomBytes(32).toString('hex');
}

// Example output: "a7f3e2b9c4d1f8e5a3b7c9d2e4f6a8b1c3d5e7f9a1b3c5d7e9f1a3b5c7d9e1f3"

Security requirements:

  • Minimum 32 characters
  • Cryptographically random (not Math.random())
  • Store hashed version in database (like passwords)

2. Hash Keys Before Storage

import bcrypt from 'bcrypt';

async function hashApiKey(apiKey: string): Promise<string> {
  const saltRounds = 10;
  return bcrypt.hash(apiKey, saltRounds);
}

// When creating a new API key:
const apiKey = generateApiKey();
const hashedKey = await hashApiKey(apiKey);

// Store hashedKey in database
await db.apiKeys.create({
  userId: user.id,
  keyHash: hashedKey,
  name: 'Production API Key',
  createdAt: new Date(),
});

// Show apiKey to user ONCE (they can't retrieve it later)
return { apiKey }; // Display this to user, never show again

3. Verify API Keys

import { Request, Response, NextFunction } from 'express';

async function authenticateApiKey(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const apiKey = req.headers['x-api-key'] as string;

  if (!apiKey) {
    return res.status(401).json({
      error: 'API key required',
      hint: 'Include X-API-Key header',
    });
  }

  // Find all active API keys (hashed)
  const apiKeys = await db.apiKeys.findMany({
    where: { active: true },
  });

  // Check if provided key matches any hashed key
  let matchedKey = null;
  for (const key of apiKeys) {
    const isMatch = await bcrypt.compare(apiKey, key.keyHash);
    if (isMatch) {
      matchedKey = key;
      break;
    }
  }

  if (!matchedKey) {
    return res.status(401).json({
      error: 'Invalid API key',
    });
  }

  // Update last used timestamp
  await db.apiKeys.update({
    where: { id: matchedKey.id },
    data: { lastUsedAt: new Date() },
  });

  // Attach user to request
  req.user = await db.users.findUnique({
    where: { id: matchedKey.userId },
  });

  next();
}

API Key Best Practices

1. Use separate keys for different environments:

// Prefix keys to prevent accidental usage
const testKey = `test_${generateApiKey()}`;
const prodKey = `prod_${generateApiKey()}`;

2. Allow multiple keys per user: Users should be able to create separate keys for different applications and rotate them independently.

3. Implement key rotation:

interface ApiKey {
  id: string;
  userId: string;
  keyHash: string;
  name: string;
  createdAt: Date;
  lastUsedAt: Date | null;
  expiresAt: Date | null;
  active: boolean;
}

// Automatically expire old keys
async function deactivateExpiredKeys() {
  await db.apiKeys.updateMany({
    where: {
      expiresAt: { lte: new Date() },
      active: true,
    },
    data: { active: false },
  });
}

4. Rate limit by API key: Track usage per key and enforce rate limits (see our API Rate Limiting Guide).

JWT (JSON Web Tokens)

What is JWT?

JWT is a compact, self-contained way to securely transmit information between parties. The token itself contains the user's identity and permissions.

Structure:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decodes to:

// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

// Signature (verifies integrity)

Implementing JWT Authentication

1. Generate JWT Tokens

import jwt from 'jsonwebtoken';

interface TokenPayload {
  userId: string;
  email: string;
  role: string;
}

function generateAccessToken(user: User): string {
  const payload: TokenPayload = {
    userId: user.id,
    email: user.email,
    role: user.role,
  };

  // Short-lived access token (15 minutes)
  return jwt.sign(payload, process.env.JWT_SECRET!, {
    expiresIn: '15m',
    issuer: 'api.yourcompany.com',
    audience: 'api.yourcompany.com',
  });
}

function generateRefreshToken(userId: string): string {
  // Long-lived refresh token (7 days)
  return jwt.sign(
    { userId },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d' }
  );
}

Security requirements:

  • Store JWT_SECRET in environment variables
  • Use strong secrets (minimum 256 bits)
  • Generate with: openssl rand -base64 32

2. Verify JWT Tokens

async function authenticateJWT(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'Authentication required',
      hint: 'Include Authorization: Bearer <token> header',
    });
  }

  const token = authHeader.substring(7); // Remove 'Bearer ' prefix

  try {
    const payload = jwt.verify(
      token,
      process.env.JWT_SECRET!
    ) as TokenPayload;

    // Optionally: Check if user still exists and is active
    const user = await db.users.findUnique({
      where: { id: payload.userId },
    });

    if (!user || !user.active) {
      throw new Error('User not found or inactive');
    }

    req.user = user;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      return res.status(401).json({
        error: 'Token expired',
        code: 'TOKEN_EXPIRED',
        hint: 'Refresh your access token',
      });
    }

    if (error instanceof jwt.JsonWebTokenError) {
      return res.status(401).json({
        error: 'Invalid token',
        code: 'INVALID_TOKEN',
      });
    }

    return res.status(500).json({
      error: 'Authentication failed',
    });
  }
}

3. Refresh Token Flow

Access tokens should be short-lived (15 minutes). Use refresh tokens to get new access tokens without re-authenticating.

// POST /auth/refresh
async function refreshAccessToken(req: Request, res: Response) {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    return res.status(400).json({
      error: 'Refresh token required',
    });
  }

  try {
    // Verify refresh token
    const payload = jwt.verify(
      refreshToken,
      process.env.JWT_REFRESH_SECRET!
    ) as { userId: string };

    // Check if refresh token is in deny list (if implemented)
    const isDenied = await isRefreshTokenDenied(refreshToken);
    if (isDenied) {
      throw new Error('Token revoked');
    }

    // Get user
    const user = await db.users.findUnique({
      where: { id: payload.userId },
    });

    if (!user || !user.active) {
      throw new Error('User not found');
    }

    // Generate new access token
    const newAccessToken = generateAccessToken(user);

    return res.json({
      accessToken: newAccessToken,
    });
  } catch (error) {
    return res.status(401).json({
      error: 'Invalid refresh token',
      hint: 'Please log in again',
    });
  }
}

JWT Security Best Practices

1. Use HTTPS only: Tokens transmitted over HTTP can be intercepted.

2. Store tokens securely on client:

// ❌ BAD: localStorage is vulnerable to XSS
localStorage.setItem('token', accessToken);

// ✅ GOOD: httpOnly cookies (not accessible to JavaScript)
res.cookie('accessToken', accessToken, {
  httpOnly: true,
  secure: true, // HTTPS only
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000, // 15 minutes
});

3. Implement token deny lists for logout:

// Store logged-out tokens in Redis with TTL = token expiry
async function logoutUser(token: string) {
  const decoded = jwt.decode(token) as { exp: number };
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);

  await redis.setex(`denied:${token}`, ttl, '1');
}

async function isTokenDenied(token: string): Promise<boolean> {
  const denied = await redis.get(`denied:${token}`);
  return denied === '1';
}

4. Include token metadata:

const payload = {
  userId: user.id,
  email: user.email,
  role: user.role,
  iat: Math.floor(Date.now() / 1000), // Issued at
  exp: Math.floor(Date.now() / 1000) + 900, // Expires in 15 min
  jti: crypto.randomUUID(), // Unique token ID
};

OAuth 2.0

OAuth 2.0 is an authorization framework that enables third-party applications to access user resources without sharing passwords.

OAuth 2.0 Flows

1. Authorization Code Flow (most secure, for web apps) 2. Client Credentials Flow (machine-to-machine) 3. Implicit Flow (deprecated, don't use) 4. Password Flow (only for trusted first-party apps)

Authorization Code Flow (Recommended)

This is what "Login with Google" uses. Example: building a Slack bot that posts to user channels.

Flow:

User → Your App → Slack OAuth → User Approves → Slack → Your App → Access Token

Implementation:

import axios from 'axios';

// Step 1: Redirect user to authorization URL
app.get('/auth/slack', (req, res) => {
  const authUrl = new URL('https://slack.com/oauth/v2/authorize');
  authUrl.searchParams.set('client_id', process.env.SLACK_CLIENT_ID!);
  authUrl.searchParams.set('scope', 'chat:write,channels:read');
  authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/auth/slack/callback');
  authUrl.searchParams.set('state', generateRandomState()); // CSRF protection

  res.redirect(authUrl.toString());
});

// Step 2: Handle callback with authorization code
app.get('/auth/slack/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state matches (CSRF protection)
  if (!verifyState(state as string)) {
    return res.status(400).send('Invalid state');
  }

  try {
    // Step 3: Exchange code for access token
    const response = await axios.post('https://slack.com/api/oauth.v2.access', {
      client_id: process.env.SLACK_CLIENT_ID,
      client_secret: process.env.SLACK_CLIENT_SECRET,
      code,
      redirect_uri: 'https://yourapp.com/auth/slack/callback',
    });

    const { access_token, team, authed_user } = response.data;

    // Step 4: Store access token securely
    await db.oauthTokens.create({
      data: {
        userId: req.session.userId, // Your app's user
        provider: 'slack',
        accessToken: encrypt(access_token), // Encrypt before storage
        teamId: team.id,
        slackUserId: authed_user.id,
        scopes: 'chat:write,channels:read',
        createdAt: new Date(),
      },
    });

    res.redirect('/dashboard?connected=slack');
  } catch (error) {
    console.error('OAuth error:', error);
    res.status(500).send('Authentication failed');
  }
});

// Step 5: Use the access token to make API requests
async function postToSlack(userId: string, channel: string, text: string) {
  const token = await db.oauthTokens.findFirst({
    where: { userId, provider: 'slack' },
  });

  if (!token) {
    throw new Error('Slack not connected');
  }

  const decryptedToken = decrypt(token.accessToken);

  await axios.post(
    'https://slack.com/api/chat.postMessage',
    { channel, text },
    {
      headers: {
        'Authorization': `Bearer ${decryptedToken}`,
        'Content-Type': 'application/json',
      },
    }
  );
}

OAuth Security Best Practices

1. Always use state parameter (CSRF protection):

function generateRandomState(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Store in session
req.session.oauthState = generateRandomState();

// Verify on callback
function verifyState(receivedState: string): boolean {
  const valid = receivedState === req.session.oauthState;
  delete req.session.oauthState; // Use once
  return valid;
}

2. Use PKCE for additional security:

// For mobile apps and SPAs
function generateCodeVerifier(): string {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier: string): string {
  return crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

3. Validate redirect URIs: Never allow arbitrary redirect URIs. Whitelist allowed URLs in your OAuth app configuration.

4. Encrypt stored tokens:

import crypto from 'crypto';

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; // 32 bytes
const IV_LENGTH = 16;

function encrypt(text: string): string {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return iv.toString('hex') + ':' + encrypted;
}

function decrypt(text: string): string {
  const parts = text.split(':');
  const iv = Buffer.from(parts[0], 'hex');
  const encryptedText = parts[1];
  const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

Security Best Practices

1. Always Use HTTPS

Never send authentication credentials over HTTP.

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect(`https://${req.headers.host}${req.url}`);
  }
  next();
});

2. Implement Rate Limiting

Prevent brute force attacks on authentication endpoints.

import rateLimit from 'express-rate-limit';

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts
  message: {
    error: 'Too many authentication attempts',
    hint: 'Please try again in 15 minutes',
  },
  standardHeaders: true,
  legacyHeaders: false,
});

app.post('/auth/login', authLimiter, async (req, res) => {
  // Login logic
});

See our complete API Rate Limiting Guide for production patterns.

3. Hash Passwords Properly

import bcrypt from 'bcrypt';

// Creating user
async function createUser(email: string, password: string) {
  const hashedPassword = await bcrypt.hash(password, 10);

  await db.users.create({
    data: {
      email,
      password: hashedPassword,
    },
  });
}

// Verifying password
async function verifyPassword(email: string, password: string): Promise<User | null> {
  const user = await db.users.findUnique({ where: { email } });

  if (!user) {
    // Don't reveal if user exists
    await bcrypt.hash(password, 10); // Prevent timing attacks
    return null;
  }

  const isValid = await bcrypt.compare(password, user.password);

  if (!isValid) {
    return null;
  }

  return user;
}

4. Implement CORS Properly

import cors from 'cors';

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'https://yourapp.com',
  credentials: true, // Allow cookies
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
}));

5. Add Security Headers

import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", 'data:', 'https:'],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

6. Validate Input

import { z } from 'zod';

const LoginSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

app.post('/auth/login', async (req, res) => {
  try {
    const { email, password } = LoginSchema.parse(req.body);
    // Proceed with authentication
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.errors,
      });
    }
  }
});

7. Log Security Events

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
  ],
});

// Log authentication attempts
logger.info('Authentication attempt', {
  email: user.email,
  ip: req.ip,
  userAgent: req.headers['user-agent'],
  success: true,
  timestamp: new Date().toISOString(),
});

// Log suspicious activity
logger.warn('Multiple failed login attempts', {
  email: email,
  ip: req.ip,
  attempts: 5,
  timestamp: new Date().toISOString(),
});

Common Security Vulnerabilities

1. Exposed Secrets in Code

// ❌ NEVER do this
const API_KEY = 'sk_live_abc123';

// ✅ Use environment variables
const API_KEY = process.env.API_KEY;

if (!API_KEY) {
  throw new Error('API_KEY environment variable required');
}

Prevention:

  • Use .env files (never commit them)
  • Use secret management (AWS Secrets Manager, HashiCorp Vault)
  • Scan code with tools like git-secrets or trufflehog

2. Timing Attacks

// ❌ Vulnerable to timing attacks
function compareTokens(userToken: string, validToken: string): boolean {
  return userToken === validToken; // Returns immediately when first character differs
}

// ✅ Use constant-time comparison
import crypto from 'crypto';

function compareTokens(userToken: string, validToken: string): boolean {
  const userBuffer = Buffer.from(userToken);
  const validBuffer = Buffer.from(validToken);

  if (userBuffer.length !== validBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(userBuffer, validBuffer);
}

3. Insecure Token Storage

// ❌ NEVER store tokens in localStorage (vulnerable to XSS)
localStorage.setItem('token', accessToken);

// ✅ Use httpOnly cookies
res.cookie('accessToken', accessToken, {
  httpOnly: true, // Not accessible to JavaScript
  secure: true, // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 15 * 60 * 1000, // 15 minutes
});

4. Missing Token Expiration

// ❌ Tokens that never expire
const token = jwt.sign({ userId: user.id }, SECRET);

// ✅ Short-lived access tokens + refresh tokens
const accessToken = jwt.sign(
  { userId: user.id },
  SECRET,
  { expiresIn: '15m' }
);

const refreshToken = jwt.sign(
  { userId: user.id },
  REFRESH_SECRET,
  { expiresIn: '7d' }
);

5. Weak Secrets

# ❌ Weak secret
JWT_SECRET=secret123

# ✅ Generate strong secret (256 bits minimum)
openssl rand -base64 32
# Output: 8Zr3vN9mX2fQ7jK5pL1wT6yU4iO0sA9dF8gH3bC7vN2=

Testing & Monitoring

Testing Authentication

import request from 'supertest';

describe('Authentication', () => {
  it('should reject requests without token', async () => {
    const response = await request(app)
      .get('/api/protected')
      .expect(401);

    expect(response.body.error).toBe('Authentication required');
  });

  it('should reject invalid tokens', async () => {
    const response = await request(app)
      .get('/api/protected')
      .set('Authorization', 'Bearer invalid_token')
      .expect(401);

    expect(response.body.code).toBe('INVALID_TOKEN');
  });

  it('should accept valid tokens', async () => {
    const user = await createTestUser();
    const token = generateAccessToken(user);

    const response = await request(app)
      .get('/api/protected')
      .set('Authorization', `Bearer ${token}`)
      .expect(200);
  });

  it('should reject expired tokens', async () => {
    const expiredToken = jwt.sign(
      { userId: 'test' },
      process.env.JWT_SECRET!,
      { expiresIn: '-1s' } // Already expired
    );

    const response = await request(app)
      .get('/api/protected')
      .set('Authorization', `Bearer ${expiredToken}`)
      .expect(401);

    expect(response.body.code).toBe('TOKEN_EXPIRED');
  });
});

Monitor Authentication Metrics

Track these metrics with Datadog, New Relic, or Sentry:

1. Failed authentication attempts:

// Alert when >100 failed attempts in 5 minutes
if (failedAttempts > 100) {
  alertSecurityTeam('Potential brute force attack');
}

2. Token expiration errors:

// High rate of TOKEN_EXPIRED errors may indicate:
// - Tokens expiring too quickly
// - Refresh flow not working
// - Clock skew issues

3. Unusual access patterns:

// Alert on:
// - Same token used from multiple IPs simultaneously
// - Tokens used after user logout
// - Tokens used from unexpected geolocations

Production Checklist

Before launching your authentication system:

Security

  • All secrets stored in environment variables or secret manager
  • HTTPS enforced (HTTP redirects to HTTPS)
  • Passwords hashed with bcrypt (salt rounds ≥10)
  • API keys stored hashed in database
  • JWT secrets are 256+ bits
  • Access tokens expire (15 minutes recommended)
  • Refresh tokens expire (7 days recommended)
  • Rate limiting on authentication endpoints (5 attempts / 15 min)
  • CORS configured with specific origins (not *)
  • Security headers implemented (Helmet.js)
  • Input validation on all authentication endpoints
  • Constant-time comparison for tokens
  • httpOnly cookies for token storage
  • CSRF protection (state parameter for OAuth)
  • XSS protection (Content Security Policy)
  • OAuth tokens encrypted before database storage

Monitoring

  • Failed authentication attempts logged
  • Successful logins logged with IP/user-agent
  • Token expiration errors tracked
  • Unusual access patterns alerted
  • Security logs sent to Datadog/Sentry
  • Dashboard showing auth success/failure rates

User Experience

  • Clear error messages (without leaking info)
  • Token refresh flow implemented
  • Logout invalidates tokens
  • Password reset flow implemented
  • Multi-factor authentication considered
  • Remember me option (optional)

Documentation

  • Authentication methods documented
  • API key generation process documented
  • Token format and claims documented
  • Error codes documented
  • Rate limits communicated
  • Security best practices shared with API consumers

Real-World Examples

Stripe's API Key Pattern

Stripe uses prefixed API keys to prevent accidental misuse:

sk_test_... // Secret key, test mode
sk_live_... // Secret key, production
pk_test_... // Publishable key, test mode
pk_live_... // Publishable key, production

Benefits:

  • Can't accidentally use test keys in production
  • Immediately identify key type
  • Can implement different validation logic per prefix

Auth0's JWT Claims

Auth0 includes rich metadata in JWT tokens:

{
  "sub": "auth0|507f1f77bcf86cd799439011",
  "email": "user@example.com",
  "email_verified": true,
  "iss": "https://yourapp.auth0.com/",
  "aud": "your_client_id",
  "iat": 1516239022,
  "exp": 1516325422,
  "scope": "openid profile email",
  "https://yourapp.com/roles": ["admin", "user"]
}

Custom claims use namespaced keys to avoid collisions.

GitHub's OAuth Scopes

GitHub uses granular permission scopes:

repo              # Full repo access
repo:status       # Just commit status
public_repo       # Public repos only
read:org          # Read org data
write:discussion  # Create discussions

Users see exactly what permissions your app requests.

Conclusion

Securing your API is a continuous process:

  1. Start with the right method — API keys for simple cases, JWT for stateless auth, OAuth for third-party access
  2. Follow security best practices — HTTPS, rate limiting, strong secrets, input validation
  3. Monitor actively — Track failed attempts, unusual patterns, token usage
  4. Iterate — Add MFA, refine scopes, improve error messages

Real-world API authentication from Auth0, Okta, AWS Cognito, and Stripe follows these patterns. Learn from their implementations and adapt to your needs.

For more API reliability patterns:

Monitor authentication provider uptime: Auth0 StatusOkta StatusAWS StatusFirebase StatusClerk Status

API Status Check

Stop checking API status pages manually

Get instant email alerts when OpenAI, Stripe, AWS, and 100+ APIs go down. Know before your users do.

Get Alerts — $9/mo →

Free dashboard available · 14-day trial on paid plans · Cancel anytime

Browse Free Dashboard →