Jwt Misconfiguration in Flask with Api Keys
Jwt Misconfiguration in Flask with Api Keys — how this specific combination creates or exposes the vulnerability
JWT misconfiguration in a Flask API that also exposes API keys creates a compound attack surface. When JWT handling is inconsistent and API keys are used as an additional authorization mechanism, attackers can bypass intended protections by targeting the weaker component.
Common JWT misconfigurations include not validating the signature, using none algorithm, failing to verify the issuer (iss) or audience (aud), accepting unsigned tokens, or mishandling token expiration. In Flask, developers sometimes load JWTs with jwt.decode(token, options={"verify_signature": False}) during debugging and forget to remove it, or they omit algorithm specification, which may default to expecting HS256 while the server actually accepts unsigned tokens.
Simultaneously, API keys in Flask are often passed via headers (e.g., X-API-Key) and validated with simple string comparisons or database lookups. If the API key check occurs after JWT validation, an attacker with a malformed or unsigned JWT may gain access to endpoints where the API key gate is not enforced, or the server may inadvertently treat a missing or weak key as acceptable when combined with a trusted JWT.
The risk is amplified when token validation logic is scattered, some routes rely solely on JWT while others rely on API keys, and configuration differs between environments. An attacker can probe for these inconsistencies—such as sending a valid-looking JWT with an invalid algorithm and a missing or weak API key—to identify which control is lax and pivot through the authorization boundary.
Real-world impacts include unauthorized access to user data, elevation to privileged operations, and exposure of sensitive endpoints. This aligns with OWASP API Top 10 controls related to broken object level authorization and insufficient authentication, and can be discovered by scanners running parallel checks such as BOLA/IDOR and Authentication alongside API key validation tests.
Api Keys-Specific Remediation in Flask — concrete code fixes
Remediation focuses on consistent validation, strict configuration, and clear separation of concerns between JWT and API key checks. Below are concrete, secure patterns for Flask.
1. Enforce strict JWT validation
Always specify the algorithm and validate claims. Avoid options={"verify_signature": False} in production.
import jwt
from flask import request, jsonify
def verify_jwt(token):
try:
decoded = jwt.decode(
token,
key="your-256-bit-secret",
algorithms=["HS256"],
options={"verify_signature": True, "require": ["exp", "iat", "iss", "aud"]},
issuer="myapi.example.com",
audience="my-client-app"
)
return decoded
except jwt.ExpiredSignatureError:
raise InvalidToken("Token expired")
except jwt.InvalidTokenError:
raise InvalidToken("Invalid token")
2. Centralize API key validation
Use a before-request handler to validate the API key for routes that require it, and avoid mixing it with JWT logic.
from flask import request, g
VALID_API_KEYS = {"sk_live_abc123", "sk_test_xyz789"}
def validate_api_key():
api_key = request.headers.get("X-API-Key")
if not api_key or api_key not in VALID_API_KEYS:
return jsonify({"error": "Invalid or missing API key"}), 401
g.api_key = api_key
@app.before_request
def before_request():
if request.endpoint in ["admin_export", "billing_report"]:
r = validate_api_key()
if r:
return r
3. Ensure consistent authorization across methods
Do not allow one authentication method to implicitly satisfy another. Require explicit validation for each protected route.
@app.route("/admin/users")
def admin_users():
auth = request.headers.get("Authorization")
if not auth or not auth.startswith("Bearer "):
return jsonify({"error": "Missing bearer token"}), 401
token = auth.split(" ")[1]
claims = verify_jwt(token)
if claims.get("role") != "admin":
return jsonify({"error": "Insufficient scope"}), 403
# proceed with admin logic
4. Harden key handling and storage
Store API keys as environment variables or via a secrets manager. Never hardcode them in source files.
import os
from flask import Flask
app = Flask(__name__)
app.config["VALID_API_KEYS"] = set(os.environ.get("API_KEYS", "").split(","))
5. Use middleware or extensions for separation
Consider Flask extensions for structured auth, and keep JWT and API key checks distinct to reduce misconfiguration risk.
# Example structure
@app.route("/data")
def get_data():
token = request.headers.get("Authorization")
if token and token.startswith("Bearer "):
claims = verify_jwt(token.split(" ")[1])
if claims:
return jsonify({"source": "jwt", "data": "protected"})
api_key = request.headers.get("X-API-Key")
if api_key and api_key in current_app.config["VALID_API_KEYS"]:
return jsonify({"source": "api_key", "data": "protected"})
return jsonify({"error": "Unauthorized"}), 401
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |