API Security in Depth · Authentication & Authorization for APIs
API Keys Done Right
Identification, hashing, prefixes, scopes and rotation.
API keys are simple long-lived bearer secrets for server-to-server access by customers. Do them well: generate high-entropy keys with a recognizable prefix (sk_live_…) for secret scanning, store only a hash (show the key once), attach scopes, an owner and expiry, support multiple active keys for zero-downtime rotation, record last used, and register the prefix with GitHub secret scanning so leaked keys are reported. Never use API keys from browsers or mobile apps — they can't be kept secret there.
Server-to-server credentials
| Criterion | API key | OAuth client credentials | mTLS client cert |
|---|---|---|---|
| Setup for customers | Easiest | Moderate | Hardest |
| Secret type | Static bearer | Short-lived tokens from secret/key | Private key (never sent) |
| Theft impact | Valid until revoked | Until token expiry + credential revoked | Needs private key |
| Scopes | Key metadata | Token scopes | Cert → identity mapping |
| Best for | Developer APIs, simple integrations | Enterprise / partner integrations | High-assurance B2B, internal services |
import hashlib, hmac, secrets
def create_key(env: str = "live") -> tuple[str, str]:
raw = f"sk_{env}_{secrets.token_urlsafe(32)}"
digest = hashlib.sha256(raw.encode()).hexdigest() # high-entropy → fast hash is fine
return raw, digest # show raw once; store digest + last4
async def authenticate(header: str) -> ApiKey:
digest = hashlib.sha256(header.encode()).hexdigest()
key = await keys.find_by_digest(digest)
if not key or key.revoked_at or (key.expires_at and key.expires_at < utcnow()):
raise Unauthorized()
await keys.touch_last_used(key.id) # async/batched in production
return key
import { randomBytes, createHash } from "node:crypto";
export function createKey(env = "live") {
const raw = `sk_${env}_${randomBytes(32).toString("base64url")}`;
return { raw, digest: createHash("sha256").update(raw).digest("hex"), last4: raw.slice(-4) };
}
Common pitfalls
- Storing keys in plaintext.
- One key per account (no rotation without downtime).
- Keys in query strings (logged everywhere).
Key points
- Prefix + high entropy + hash at rest
- Scopes, expiry and multiple active keys
- Header only; secret scanning partnership
Further reading: GitHub secret scanning partner program
Practice questions
Why is a plain SHA-256 acceptable for API keys but not passwords?
Answer
API keys are long random values, so brute-forcing the hash is infeasible; passwords are low-entropy and need slow hashes like Argon2/bcrypt.
Why give API keys a recognizable prefix?
Answer
So secret scanners can detect leaked keys in code and logs and so humans can tell key types and environments apart.
Open in the app to mark lessons complete, take notes and track your progress.