API Key Authentication

API Key Authentication

API key authentication is a credential pattern where a long-lived, opaque token is issued to a client (typically a machine-to-machine caller or developer) to authenticate API requests — distinct from OAuth2 access tokens in that it authenticates the calling application rather than a user identity, carries no embedded claims, and requires a store lookup on every request to validate both authenticity and revocation status.


Intent

API keys are appropriate for server-to-server integrations, third-party developer access, and inbound webhook verification where user-delegated OAuth2 flows would add unnecessary complexity. The pattern trades the sophistication of OAuth2's delegation model for operational simplicity: no redirect flows, no refresh token rotation, no user context — just a shared secret issued once and validated on every request.


When NOT to Use

  • Do not use API keys for user-delegated access. A user granting a third-party access to their data requires OAuth2 (see OAuth2-OIDC-Flows). API keys authenticate applications, not users — there is no sub claim and no user consent flow.
  • Do not store API keys in client-side code (JavaScript bundles, mobile app binaries). Keys embedded in client-side code are extractable by anyone who downloads the app or inspects the source. Use OAuth2 with PKCE for browser and mobile clients.
  • Do not use a single API key with no expiry across environments. Long-lived, non-rotating keys are the most common source of credential exposure. Keys should have explicit expiry and a rotation plan from the day they are issued.
  • Do not send API keys in URL query parameters (?apikey=...). Query parameters appear in server logs, browser history, and HTTP referrer headers — they are not treated as secrets by any layer of the HTTP stack. Always transmit API keys via the Authorization header or a custom header (e.g., X-API-Key).

Key Anatomy

A well-structured API key has three components:

1. Prefix (plaintext): A short, unique string identifying the issuing service or key type — for example, sk_live_, ghp_, stripe_. The prefix enables automated secret scanning tools (GitHub secret scanning, TruffleHog, trufflehog) to detect leaked keys in repositories. Choose a unique, service-specific prefix and register it with secret scanning tools. The prefix is never a secret — it is designed to be visible in scanning output.

2. Random secret (high entropy): 32+ bytes of cryptographically random data, typically base62 or base64url encoded (producing a 43–86 character string). Use crypto.randomBytes(32) (Node.js) or SecureRandom (Java) — never use Math.random() or UUID.randomUUID() as the sole entropy source, as neither provides sufficient randomness for a credential.

3. Stored form (hashed): The server stores only the HMAC-SHA256 or bcrypt hash of the key, not the plaintext. The full key is shown to the user exactly once at creation; the server cannot recover it. On each request, the server hashes the presented key and compares it to the stored hash using a constant-time comparison to prevent timing attacks.

Format example: sk_live_A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0

  • sk_live_ — prefix (plaintext, identifies service + environment)
  • A1B2C3D4... — 32-byte random secret, base62-encoded

Dual-Active Rotation Pattern

The dual-active rotation pattern allows zero-downtime key rotation. It mirrors the dual-credential pattern from Secrets-Management:

  1. Issue Key B — generate a new key (B) while Key A remains active. Both keys are now simultaneously valid. The key store must support two active keys per client during the transition window.
  2. Distribute Key B — deliver Key B to the client via a secure channel (secrets manager, encrypted delivery). The client updates their configuration and prepares to deploy.
  3. Verify adoption — confirm the client is using Key B by monitoring which key prefix or key ID appears in incoming requests. Do not revoke Key A until adoption is confirmed.
  4. Revoke Key A — mark Key A as revoked in the key store. All subsequent requests using Key A return 401.

The transition window (steps 2–3) allows the client time to deploy the new key without a service interruption. The issuing service must be designed to support two simultaneously active keys per client.

Dual-Active Rotation Sequence

sequenceDiagram
    participant Client
    participant API as API Service
    participant KS as Key Store

    Note over API,KS: Phase 1 — Issue new key
    API->>KS: Store Key B (hash), mark active
    API-->>Client: Deliver Key B (plaintext, shown once)

    Note over Client,API: Phase 2 — Transition
    Client->>API: Requests using Key B<br/>Authorization: ApiKey sk_live_B...
    API->>KS: Validate Key B hash → active
    API-->>Client: 200 OK

    Note over API,KS: Phase 3 — Revoke old key
    API->>KS: Mark Key A revoked
    Client->>API: (old client): Requests using Key A
    API->>KS: Validate Key A hash → revoked
    API-->>Client: 401 Unauthorized

Lifecycle Comparison — API Keys vs OAuth2 Access Tokens

DimensionAPI KeyOAuth2 Access Token
Issued toApplication (machine identity)User or application (delegated access)
FormatOpaque random stringJWT (claims-bearing) or opaque
ExpiryLong-lived (days to never); explicit rotation scheduleShort-lived (minutes to hours); auto-refreshed via refresh token
RevocationHash lookup in key store; immediate effectToken introspection endpoint or short TTL + refresh token revocation
ClaimsNone embedded — server must look up permissionsClaims embedded (sub, scope, exp, etc.)
RotationManual or scripted dual-active rotationAutomatic via refresh token rotation
Client typesServer-to-server, developer integrations, webhooksAll client types including browsers and mobile
ScopePer-key permission set defined at issuancePer-token scope requested at authorization time

For OAuth2 token lifecycle details, see OAuth2-OIDC-Flows. For JWT claim validation, see JWT.


Revocation Model

API key revocation must take immediate effect. Because API keys are opaque (no embedded expiry claim like a JWT), every request requires a key store lookup to validate the key hash and check revocation status.

The lookup result can be cached with a short TTL (30–60 seconds) to reduce latency, but the cache must be invalidated on revocation events via a pub/sub invalidation mechanism. Do not rely on TTL expiry alone for security-sensitive revocation (key compromise, employee offboarding) — the 30–60 second window may be acceptable for routine operations but is too long when immediate revocation is required.

Design for immediate revocation: central key store with cache-aside invalidation. On a revocation event, write the revoked status to the store and publish an invalidation message to all caching instances.


TypeScript Example

import { createHmac, timingSafeEqual } from 'node:crypto';
 
interface KeyRecord { hashedKey: string; revoked: boolean; clientId: string; }
 
async function validateApiKey(
  rawKey: string,
  keyStore: Map<string, KeyRecord>  // In production: database or Redis
): Promise<{ valid: boolean; clientId?: string }> {
  if (!rawKey.startsWith('sk_live_')) {
    return { valid: false }; // Prefix check — fails fast on malformed keys
  }
 
  // Hash the presented key: store never holds plaintext
  const hash = createHmac('sha256', process.env['HMAC_SECRET']!)
    .update(rawKey)
    .digest('hex');
 
  const record = keyStore.get(hash);
  if (!record || record.revoked) {
    return { valid: false };
  }
 
  return { valid: true, clientId: record.clientId };
}

timingSafeEqual from node:crypto prevents timing attacks when comparing hashes in lower-level implementations. For hash map lookups, the constant-time property is provided by the hash function itself, but explicit timingSafeEqual is required when doing byte-by-byte comparisons.


Java Example

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
 
public class ApiKeyAuthFilter extends OncePerRequestFilter {
 
    private final ApiKeyRepository keyRepository;
    private final byte[] hmacSecret;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws Exception {
        String rawKey = request.getHeader("Authorization");
        if (rawKey == null || !rawKey.startsWith("ApiKey ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        String key = rawKey.substring(7); // Strip "ApiKey " prefix
        String hash = hmacSha256(key, hmacSecret);
        ApiKeyRecord record = keyRepository.findByHash(hash);
 
        if (record == null || record.isRevoked()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        // Set security context with clientId principal
        SecurityContextHolder.getContext().setAuthentication(
            new ApiKeyAuthenticationToken(record.getClientId())
        );
        chain.doFilter(request, response);
    }
 
    private String hmacSha256(String data, byte[] secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret, "HmacSHA256"));
        return bytesToHex(mac.doFinal(data.getBytes()));
    }
}

OncePerRequestFilter guarantees the filter executes exactly once per request dispatch, which is important for security filters that must not be invoked multiple times in a forward/include chain.


ConceptRelationship
OAuth2-OIDC-FlowsOAuth2 access tokens are the user-delegated alternative for multi-tenant or user-consent scenarios
JWTJWT access tokens embed claims and expiry; API keys are opaque and require a store lookup
Secrets-ManagementAPI keys are a category of secret; the dual-active rotation pattern mirrors dual-credential rotation
Input-ValidationAPI key format (prefix + high-entropy string) must be validated at ingress before any store lookup
Zero-Trust-ArchitectureAPI keys can serve as workload identity signals in the workload plane of a ZTA deployment