In this lab, you'll learn how API keys are commonly used to secure REST APIs. You'll see how keys are passed (usually in the Authorization header as Bearer <token> or in custom headers), and how to implement simple API key authentication in Flask.
- Most APIs require a key for access.
- Keys are often passed as:
Authorization: Bearer <token>- Or a custom header (e.g.,
X-API-Key: <token>)
- Sometimes keys are in the URL or body, but headers are preferred for security.
Rules of the Road:
- Treat API keys like passwords: never commit them to Git
- Do not put keys in URLs (they end up in logs and browser history)
- Prefer
Authorization: Bearer <key>orX-API-Key: <key> - Keys should be:
- Random (use
secrets) - Rotatable (reset endpoint)
- Revocable (disable/delete key)
- Random (use
Following these rules keeps your API secure and maintainable.
Real-world note:
- API keys identify the caller and allow access control
- They do not encrypt traffic
- Without HTTPS, keys can be stolen on the network
- In real systems, keys are stored hashed (like passwords), not in plaintext
This sets up the need for HTTPS and secure storage in future lessons.
What is a hash? A hash function takes an input (like an API key) and produces a fixed-length output. Hash functions are one-way: you cannot recover the original key from the hash.
When storing API keys, we store only the hash, not the key itself. When a client presents a key, we hash it and compare it to the stored hash.
Why hash?
- If your database is leaked, attackers can’t use the hashes as API keys.
- It’s a security best practice (like password storage).
What is SHA-256?
SHA-256 is a cryptographic hash function that produces a 256-bit (32-byte) output. It is designed to be:
- One-way (cannot be reversed)
- Collision-resistant (hard to find two inputs with the same hash)
- Deterministic (same input → same output)
- Widely trusted and standardized
This makes SHA-256 suitable for hashing API keys and other secrets.
Python provides a built-in hash() function, but it is not cryptographically secure.
What is HMAC (and why use it)?
HMAC (Hash-based Message Authentication Code) combines:
HMAC uses a single server-side secret combined with SHA-256. This secret is not random per key and is not stored in the database. It ensures that even if hashes are stolen, attackers cannot verify guessed keys offline without access to the server secret.
- a cryptographic hash function (SHA-256)
- a secret known only to the server (stored in env variables, config files, or secret managers - not in the database)
- Unlike a salt, the HMAC secret is not stored with the hash and is required to verify any key.
Even if hashes are stolen from the database, attackers cannot verify guessed offline with a brute force without the secret.
Unlike passwords, API keys are long, randomly generated values, which makes cryptographic hashing practical and effective. Python’s hash() is designed for hash tables and internal use, not for security or persistence.
How to hash API keys (stdlib, safe): Use HMAC with a server secret:
import hmac, hashlib
SERVER_SECRET = b"dev-secret-change-me"
def hash_key(api_key: str) -> str:
return hmac.new(SERVER_SECRET, api_key.encode(), hashlib.sha256).hexdigest()
def keys_match(api_key: str, stored_hash: str) -> bool:
return hmac.compare_digest(hash_key(api_key), stored_hash)"""
Simple API Key Example (intentionally lacks proper hashing and events/hooks)
USAGE:
# Try with a valid key
curl -H "Username: alice" -H "Authorization: Bearer secretkey1" http://localhost:5000/hello
# Try with an invalid key
curl -H "Username: alice" -H "Authorization: Bearer wrongkey" http://localhost:5000/hello
"""
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
# In-memory user/key database
user_db = {
"alice": {"api_key": "b7f3e2c1a9d4f6e8b2c7a1e3d5f8b6c4", "role": "admin"},
"bob": {"api_key": "e4c2b1a7d9f3c6e8a2b5d7f1e3c8a6b4", "role": "user"}
}
def check_api_key():
auth = request.headers.get('Authorization', '')
username = request.headers.get('Username', '')
if not username:
abort(401, description="Missing Username header")
if auth.startswith('Bearer '):
key = auth.split(' ', 1)[1]
info = user_db.get(username)
if info and info["api_key"] == key:
return username # Authenticated
abort(401, description="Invalid or missing API key or username")
@app.route('/hello')
def hello():
user = check_api_key()
return jsonify({"message": f"Hello, {user}!"})
if __name__ == '__main__':
app.run(debug=True)- 401 Unauthorized: missing/invalid credentials (no or bad key)
- 403 Forbidden: key is valid but not allowed (wrong role, blocked user)
Use abort(401) when missing/invalid, and abort(403) when user exists but is disabled/lacks permission.
Modify your previous Flask app to require API keys for all endpoints (except user creation/reset). Implement:
- An endpoint to add a user and generate a random API key (return it in the response)
- An endpoint to reset a user's API key (generate a new one)
- Store only a hash of the API key, not the key itself. When authenticating, check the provided key against the stored hash.
- All other endpoints must require a valid API key in the
Authorizationheader - Use Flask events/hooks (before_request, after_request) to check keys, log attempts, and handle errors.
- You may use custom headers if you prefer
Tip: Use Python's secrets module to generate secure random keys:
import secrets
key = secrets.token_hex(16)Bonus:
- Log all failed authentication attempts
- Allow API keys to be passed as a custom header (e.g.,
X-API-Key) - Return a helpful error message and status code for missing/invalid keys
This lab prepares you for more advanced authentication (JWT, OAuth) in future lessons!