Skip to content

Latest commit

 

History

History
191 lines (129 loc) · 6.3 KB

File metadata and controls

191 lines (129 loc) · 6.3 KB

Lab: API Keys and Authentication in Flask

Overview

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.


Part 1: API Keys in the Real World

  • 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.

Part 1.5: API Key Rules (Practical)

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> or X-API-Key: <key>
  • Keys should be:
    • Random (use secrets)
    • Rotatable (reset endpoint)
    • Revocable (disable/delete key)

Following these rules keeps your API secure and maintainable.


Part 1.6: What API Keys Do (and Don’t Do)

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.


Part 1.7: How to Hash API Keys (and Why)

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)

Part 2: Example – Flask API with API Key Check

"""
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)

Part 2.5: Correct Status Codes

  • 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.


Part 3: Challenge – Add API Key Support to Your App

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 Authorization header
  • 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!