← Back to all posts

API Authentication Best Practices: JWT, OAuth2, and API Keys

Deep dive into JWT security, OAuth2 flows, and API key management with production-ready code examples.

Authentication is the foundation of API security. Get it wrong, and attackers can impersonate users, steal data, and compromise entire systems. This guide covers the three most common authentication patterns and how to implement them securely.

The Authentication Landscape

MethodBest ForSecurity LevelComplexity
API KeysServer-to-server, public APIsLow-MediumSimple
JWTStateless auth, microservicesMedium-HighMedium
OAuth2Third-party access, user consentHighComplex

API Keys: Simple but Limited

When to Use API Keys

✅ Good for:

  • Server-to-server communication
  • Public APIs with rate limiting
  • Simple authentication for non-sensitive endpoints
  • Internal microservices (in private networks)

❌ Bad for:

  • User authentication (no logout mechanism)
  • Sensitive data (keys don't expire automatically)
  • Browser-based apps (keys exposed in source code)

Secure API Key Implementation

// Generate cryptographically secure API keys
const crypto = require('crypto');

function generateAPIKey() {
  // 32 bytes = 256 bits of entropy
  return crypto.randomBytes(32).toString('base64url');
}

Storage:

// NEVER store plain text - always hash
const crypto = require('crypto');

function hashAPIKey(key) {
  return crypto
    .createHash('sha256')
    .update(key)
    .digest('hex');
}

// Store this hash in your database
const hashedKey = hashAPIKey(apiKey);

API Key Security Checklist

  • Always use HTTPS — API keys transmitted over HTTP are trivially stolen
  • Hash before storing — Never store plain-text keys in your database
  • Set expiration dates — Force key rotation (30-90 days for high-security)
  • Implement rate limiting — Prevent abuse even with valid keys
  • Scope permissions — Use separate keys for read vs write operations
  • Log all usage — Track when and how keys are used
  • Support key rotation — Allow users to regenerate keys without downtime
  • Detect leaked keys — Monitor GitHub, Pastebin for exposed keys

JWT: Stateless and Scalable

When to Use JWT

✅ Good for:

  • Stateless authentication (no session storage)
  • Microservices architecture
  • Mobile apps
  • APIs with horizontal scaling

❌ Bad for:

  • Long-lived sessions (can't revoke easily)
  • Highly sensitive operations (prefer short-lived + refresh tokens)
  • Situations requiring instant logout

Secure JWT Implementation

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Generate a strong secret (do this ONCE, store securely)
const JWT_SECRET = crypto.randomBytes(64).toString('hex');

function generateToken(user) {
  const payload = {
    sub: user.id,
    email: user.email,
    role: user.role,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (15 * 60)  // 15 minutes
  };
  
  return jwt.sign(payload, process.env.JWT_SECRET, {
    algorithm: 'HS256'
  });
}

function generateRefreshToken(user) {
  const payload = {
    sub: user.id,
    type: 'refresh',
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60)  // 7 days
  };
  
  return jwt.sign(payload, process.env.JWT_SECRET, {
    algorithm: 'HS256'
  });
}

Common JWT Vulnerabilities

1. Algorithm Confusion (alg: none)

Attack:

// Attacker creates token with no signature
const fakeToken = btoa(JSON.stringify({alg: "none"})) + '.' +
                  btoa(JSON.stringify({sub: "admin", role: "admin"})) + '.';

Defense:

// ALWAYS specify allowed algorithms
jwt.verify(token, secret, {
  algorithms: ['HS256']  // Whitelist only what you use
});

// Never allow 'none'
if (header.alg === 'none') {
  throw new Error('Algorithm "none" not allowed');
}

2. Weak Secrets

// VULNERABLE
const JWT_SECRET = "secret123";  // NEVER DO THIS

// SECURE
const JWT_SECRET = crypto.randomBytes(64).toString('hex');
// Store in environment variable, never commit to git

3. Sensitive Data in Payload

// VULNERABLE
const payload = {
  userId: 123,
  password: "hashed_password",  // NEVER DO THIS
  creditCard: "4111-1111-1111-1111"  // NEVER DO THIS
};

// SECURE - Only include non-sensitive identifiers
const payload = {
  sub: userId,
  email: userEmail,
  role: userRole,
  iat: now,
  exp: now + 900
};

Remember: JWT payloads are base64-encoded, NOT encrypted. Anyone can decode and read them.

JWT Best Practices

  • Short expiration — 15 minutes for access tokens, 7 days for refresh tokens
  • Use refresh tokens — Long-lived refresh tokens, short-lived access tokens
  • Validate algorithm — Whitelist allowed algorithms (HS256 or RS256)
  • Strong secrets — Minimum 256 bits (32 bytes) of entropy
  • Include claimsiss, sub, iat, exp
  • Validate all claims — Check expiration, issuer, audience
  • No sensitive data — JWT payloads are readable by anyone
  • HTTPS only — Never send JWTs over unencrypted connections
  • Secure storage — HttpOnly cookies (web) or secure storage (mobile)

OAuth2: Third-Party Access

When to Use OAuth2

✅ Good for:

  • "Sign in with Google/GitHub" functionality
  • Third-party API access (e.g., posting to user's Twitter)
  • Delegated authorization (user grants app limited access)
  • Mobile app authentication

❌ Bad for:

  • First-party authentication (JWT is simpler)
  • Server-to-server (API keys are simpler)
  • When you don't need third-party access

Authorization Code Flow (Most Secure)

Best for: Web apps with backend servers

1. User clicks "Login with GitHub"
2. Redirect to: https://github.com/login/oauth/authorize?
   client_id=YOUR_ID&
   redirect_uri=https://yourapp.com/callback&
   scope=user:email&
   state=RANDOM_STRING

3. User approves → GitHub redirects to:
   https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STRING

4. Your backend exchanges code for token:
   POST https://github.com/login/oauth/access_token
   {
     client_id: YOUR_ID,
     client_secret: YOUR_SECRET,
     code: AUTH_CODE
   }

5. Receive access_token → use to call GitHub API

OAuth2 Security Checklist

  • Always use state parameter — Prevents CSRF attacks
  • Validate redirect_uri — Exact match, no wildcards
  • HTTPS only — Never use OAuth over HTTP
  • Short-lived authorization codes — 10 minutes maximum
  • Store client_secret securely — Never expose in frontend code
  • Validate all responses — Check state, error codes
  • Limit scopes — Request minimum permissions needed
  • Use PKCE for mobile — Proof Key for Code Exchange

Authentication Decision Tree

Do you need third-party access? (user's Google/GitHub account)
├─ YES → OAuth2
└─ NO ↓

Is this server-to-server communication?
├─ YES → API Keys (with rotation + rate limits)
└─ NO ↓

Do you need stateless authentication?
├─ YES → JWT (with refresh tokens)
└─ NO → Session-based auth (cookies)

Conclusion

Choose your authentication method based on your use case:

  • API Keys: Simple server-to-server or public APIs
  • JWT: Stateless user auth, microservices, mobile apps
  • OAuth2: Third-party access, "Sign in with..." flows

No matter which you choose:

  1. Always use HTTPS
  2. Implement proper expiration
  3. Hash/encrypt secrets
  4. Add rate limiting
  5. Log authentication attempts
  6. Test thoroughly

ThreeStack helps fintech companies build secure authentication systems. Get a free security assessment or learn more about our services.

Test Your API Security Now

Our free scanner checks for 34 security issues including CORS, authentication, and rate limiting.

Run Free Security Scan →