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
subclaim 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 theAuthorizationheader 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:
- 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.
- 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.
- 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.
- 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
| Dimension | API Key | OAuth2 Access Token |
|---|---|---|
| Issued to | Application (machine identity) | User or application (delegated access) |
| Format | Opaque random string | JWT (claims-bearing) or opaque |
| Expiry | Long-lived (days to never); explicit rotation schedule | Short-lived (minutes to hours); auto-refreshed via refresh token |
| Revocation | Hash lookup in key store; immediate effect | Token introspection endpoint or short TTL + refresh token revocation |
| Claims | None embedded — server must look up permissions | Claims embedded (sub, scope, exp, etc.) |
| Rotation | Manual or scripted dual-active rotation | Automatic via refresh token rotation |
| Client types | Server-to-server, developer integrations, webhooks | All client types including browsers and mobile |
| Scope | Per-key permission set defined at issuance | Per-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.
Related Concepts
| Concept | Relationship |
|---|---|
| OAuth2-OIDC-Flows | OAuth2 access tokens are the user-delegated alternative for multi-tenant or user-consent scenarios |
| JWT | JWT access tokens embed claims and expiry; API keys are opaque and require a store lookup |
| Secrets-Management | API keys are a category of secret; the dual-active rotation pattern mirrors dual-credential rotation |
| Input-Validation | API key format (prefix + high-entropy string) must be validated at ingress before any store lookup |
| Zero-Trust-Architecture | API keys can serve as workload identity signals in the workload plane of a ZTA deployment |