BytePane

API Authentication Methods: API Keys, OAuth, JWT & mTLS Compared

Security15 min read

Authentication vs Authorization

Before comparing methods, it is important to distinguish authentication (who are you?) from authorization (what can you do?). Authentication verifies identity; authorization determines permissions. Most API security systems handle both, but they are separate concerns.

API keys primarily handle authentication and rate limiting. OAuth 2.0 handles both through scopes. JWT tokens carry identity and authorization claims in their payload. mTLS authenticates at the transport layer before the application even processes the request.

API Keys: Simple but Limited

API keys are the simplest authentication method. The server generates a unique string, the client includes it in every request, and the server validates it against a database of known keys. Most public APIs start here because it has zero learning curve.

// API Key in header (preferred — keeps URLs clean)
GET /api/v1/data
Authorization: Bearer bpk_live_a1b2c3d4e5f6g7h8
X-API-Key: bpk_live_a1b2c3d4e5f6g7h8

// API Key in query parameter (avoid — gets logged in server logs)
GET /api/v1/data?api_key=bpk_live_a1b2c3d4e5f6g7h8

// Server-side validation (Node.js/Express)
app.use('/api', (req, res, next) => {
  const apiKey = req.headers['x-api-key']
  if (!apiKey) return res.status(401).json({ error: 'API key required' })

  const keyRecord = await db.apiKeys.findByKey(hash(apiKey))
  if (!keyRecord) return res.status(403).json({ error: 'Invalid API key' })
  if (keyRecord.revoked) return res.status(403).json({ error: 'Key revoked' })

  req.client = keyRecord.client
  next()
})

API keys have significant limitations: they cannot represent user context (who is the end user?), they grant the same permissions for every request, and if leaked, they provide full access until manually revoked. Always hash API keys before storing them in your database.

OAuth 2.0: The Industry Standard

OAuth 2.0 is the authorization framework used by Google, GitHub, Facebook, and virtually every major API. It allows users to grant third-party applications limited access to their resources without sharing their password.

// OAuth 2.0 Authorization Code Flow (most common for web apps)

// Step 1: Redirect user to authorization server
// GET https://auth.example.com/authorize?
//   response_type=code&
//   client_id=your_client_id&
//   redirect_uri=https://yourapp.com/callback&
//   scope=read:user read:repos&
//   state=random_csrf_token

// Step 2: User approves, redirected back with auth code
// GET https://yourapp.com/callback?code=abc123&state=random_csrf_token

// Step 3: Exchange code for tokens (server-to-server)
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: 'abc123',
    client_id: process.env.OAUTH_CLIENT_ID,
    client_secret: process.env.OAUTH_CLIENT_SECRET,
    redirect_uri: 'https://yourapp.com/callback',
  }),
})

// Step 4: Receive access token + refresh token
// { access_token: "eyJ...", refresh_token: "dGhp...",
//   token_type: "Bearer", expires_in: 3600, scope: "read:user read:repos" }

// Step 5: Use access token for API calls
const user = await fetch('https://api.example.com/user', {
  headers: { Authorization: 'Bearer eyJ...' },
})

JWT Bearer Tokens: Stateless Authentication

JSON Web Tokens encode identity and authorization claims directly in the token, eliminating the need for server-side session storage. The server signs the token with a secret key, and any service with the same key can verify it without a database lookup.

// JWT structure: header.payload.signature
// Decode with BytePane's JWT Decoder tool

// Header: {"alg": "RS256", "typ": "JWT"}
// Payload:
{
  "sub": "user_42",
  "email": "[email protected]",
  "roles": ["admin", "editor"],
  "iat": 1709900000,
  "exp": 1709903600,  // Expires in 1 hour
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

// Server-side verification (Node.js)
import jwt from 'jsonwebtoken'

function verifyToken(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (!token) return res.status(401).json({ error: 'Token required' })

  try {
    const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com',
    })
    req.user = decoded
    next()
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' })
    }
    return res.status(403).json({ error: 'Invalid token' })
  }
}

Use our JWT Decoder to inspect token contents during development. For a deeper dive into JWT structure and security, see our JWT tokens explained guide and our comparison of JWT vs session cookies.

Mutual TLS (mTLS): Certificate-Based Authentication

Mutual TLS extends standard HTTPS by requiring the client to present a certificate alongside the server. Both sides verify each other's identity using X.509 certificates signed by a trusted Certificate Authority (CA). This is the gold standard for service-to-service communication in zero-trust architectures.

// mTLS handshake flow:
// 1. Client connects to server
// 2. Server presents its certificate (standard TLS)
// 3. Server requests client certificate (mTLS addition)
// 4. Client presents its certificate
// 5. Server verifies client cert against trusted CA
// 6. Both sides derive encryption keys

// Node.js server with mTLS
const https = require('https')
const fs = require('fs')

const server = https.createServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  ca: fs.readFileSync('ca-cert.pem'),      // Trusted CA
  requestCert: true,                        // Require client cert
  rejectUnauthorized: true,                 // Reject invalid certs
}, (req, res) => {
  const clientCert = req.socket.getPeerCertificate()
  console.log('Client:', clientCert.subject.CN)  // Common Name
  res.end('Authenticated via mTLS')
})

// Client with mTLS certificate
const response = await fetch('https://api.internal.com/data', {
  agent: new https.Agent({
    cert: fs.readFileSync('client-cert.pem'),
    key: fs.readFileSync('client-key.pem'),
    ca: fs.readFileSync('ca-cert.pem'),
  }),
})

Comparison Table: All Methods Side by Side

FeatureAPI KeysOAuth 2.0JWTmTLS
ComplexityLowHighMediumHigh
User contextNoYesYesNo
Token expiryManualAutoAutoCert expiry
RevocationDB lookupToken storeBlocklistCRL/OCSP
Scoped accessLimitedFullVia claimsLimited
Best forServer-to-serverUser delegationMicroservicesZero-trust infra

Refresh Token Rotation

Access tokens should have short lifetimes (5-60 minutes) to limit damage if stolen. Refresh tokens allow clients to obtain new access tokens without re-authenticating the user. Refresh token rotation issues a new refresh token with every use, detecting stolen tokens when the old one is reused.

// Refresh token rotation flow
async function refreshAccessToken(refreshToken: string) {
  const response = await fetch('/auth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    }),
  })

  if (response.status === 401) {
    // Refresh token was revoked or reused (possible theft)
    // Force user to re-authenticate
    logout()
    return
  }

  const { access_token, refresh_token: newRefreshToken } = await response.json()

  // Store new tokens (old refresh token is now invalid)
  setAccessToken(access_token)        // In memory only
  setRefreshToken(newRefreshToken)     // httpOnly cookie
}

// Automatic token refresh on 401 (axios interceptor)
api.interceptors.response.use(
  response => response,
  async error => {
    if (error.response?.status === 401 && !error.config._retry) {
      error.config._retry = true
      await refreshAccessToken(getRefreshToken())
      return api(error.config)  // Retry original request
    }
    return Promise.reject(error)
  }
)

Choosing the Right Method

The best authentication method depends on your use case. Many production systems combine multiple methods: API keys for external integrations, OAuth for user-facing features, JWTs for internal microservice communication, and mTLS for infrastructure-level trust.

  • Public data API -- API keys with rate limiting
  • SaaS product -- OAuth 2.0 with JWT access tokens
  • Mobile app backend -- OAuth 2.0 PKCE flow with refresh token rotation
  • Internal microservices -- JWT with shared signing key or mTLS
  • Third-party integrations -- OAuth 2.0 with scoped permissions
  • Zero-trust infrastructure -- mTLS with service mesh (Istio, Linkerd)

Debug API Authentication with BytePane

Decode and inspect JWT tokens with our JWT Decoder. Encode API keys for safe transport with the Base64 Encoder. Generate secure random secrets with the Password Generator.

Open JWT Decoder

Related Articles