Authentication¶
Every API that handles user data needs authentication — a way to verify who is making a request. This guide covers the most common patterns: API keys, JWT tokens, and how to build reusable auth guards with Responder’s before-request hooks.
API Key Authentication¶
The simplest approach. The client sends a secret key in a header, and your server checks it against a known value. This is common for server-to-server communication and simple APIs:
API_KEYS = {"sk-abc123", "sk-def456"}
@api.route(before_request=True)
def check_api_key(req, resp):
key = req.headers.get("X-API-Key")
if key not in API_KEYS:
resp.status_code = 401
resp.media = {"error": "Invalid or missing API key"}
Because the before-request hook sets resp.status_code, the route
handler is skipped entirely for unauthorized requests. The client never
reaches your endpoint — the guard catches them first.
The client sends the key like this:
$ curl -H "X-API-Key: sk-abc123" http://localhost:5042/protected
Bearer Token Authentication¶
Bearer tokens are the standard for modern APIs. The client sends a token
in the Authorization header, and the server validates it. The most
common format is JWT (JSON Web Tokens).
Install PyJWT:
$ uv pip install pyjwt
Create a helper to encode and decode tokens:
import jwt
from datetime import datetime, timedelta
SECRET = "your-secret-key"
def create_token(user_id: int) -> str:
payload = {
"sub": user_id,
"exp": datetime.utcnow() + timedelta(hours=24),
}
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token: str) -> dict | None:
try:
return jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.InvalidTokenError:
return None
Add a login endpoint that issues tokens, and a before-request hook that verifies them:
@api.route("/login", methods=["POST"])
async def login(req, resp):
data = await req.media()
# In a real app, check credentials against a database
if data.get("username") == "admin" and data.get("password") == "secret":
token = create_token(user_id=1)
resp.media = {"token": token}
else:
resp.status_code = 401
resp.media = {"error": "Invalid credentials"}
@api.route(before_request=True)
def auth_guard(req, resp):
# Skip auth for the login endpoint itself
if req.url.path == "/login":
return
auth = req.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
resp.status_code = 401
resp.media = {"error": "Missing bearer token"}
return
token = auth[7:] # Strip "Bearer "
payload = verify_token(token)
if payload is None:
resp.status_code = 401
resp.media = {"error": "Invalid or expired token"}
return
# Store the authenticated user on the request state
req.state.user_id = payload["sub"]
Now any route can access the authenticated user:
@api.route("/me")
def get_me(req, resp):
resp.media = {"user_id": req.state.user_id}
The client flow:
POST /loginwith credentials → receive a tokenInclude
Authorization: Bearer <token>on every subsequent requestThe token expires after 24 hours — the client must log in again
Skipping Auth for Public Routes¶
The example above skips auth for /login by checking the path. For
more control, you can use a set of public paths:
PUBLIC_PATHS = {"/login", "/signup", "/health", "/docs", "/schema.yml"}
@api.route(before_request=True)
def auth_guard(req, resp):
if req.url.path in PUBLIC_PATHS:
return
# ... check token
Custom Exception for Auth Errors¶
For cleaner code, define a custom exception and register a handler:
class AuthError(Exception):
def __init__(self, message="Unauthorized", status_code=401):
self.message = message
self.status_code = status_code
@api.exception_handler(AuthError)
async def handle_auth_error(req, resp, exc):
resp.status_code = exc.status_code
resp.media = {"error": exc.message}
Now your auth guard can simply raise:
@api.route(before_request=True)
def auth_guard(req, resp):
if req.url.path in PUBLIC_PATHS:
return
if "Authorization" not in req.headers:
raise AuthError("Missing authorization header")
Using Sessions for Web Apps¶
For traditional web applications (with HTML pages and forms), cookie-based sessions are simpler than tokens. The browser handles cookies automatically — no client-side token management needed:
@api.route("/login", methods=["POST"])
async def login(req, resp):
data = await req.media("form")
if data["username"] == "admin" and data["password"] == "secret":
resp.session["user"] = data["username"]
api.redirect(resp, location="/dashboard")
else:
resp.status_code = 401
resp.html = "<p>Invalid credentials</p>"
@api.route("/dashboard")
def dashboard(req, resp):
user = req.session.get("user")
if not user:
api.redirect(resp, location="/login")
return
resp.html = f"<h1>Welcome, {user}!</h1>"
@api.route("/logout")
def logout(req, resp):
resp.session.clear()
api.redirect(resp, location="/login")
Remember to set a proper secret key:
api = responder.API(secret_key="your-production-secret-key")
The session data is signed (not encrypted) — users can read it but can’t tamper with it. Don’t store sensitive data like passwords in sessions.