Api Key Exposure in Flask with Oauth2
Api Key Exposure in Flask with Oauth2 — how this specific combination creates or exposes the vulnerability
In Flask applications that use OAuth 2.0, API keys can be exposed through misconfiguration around token handling, logging, and introspection endpoints. OAuth 2.0 introduces bearer tokens that, if treated as opaque values and not validated or protected with the same care as API keys, can lead to sensitive data exposure. Common patterns that increase risk include logging full authorization headers, caching introspection responses without redaction, and failing to restrict token scopes to the minimum required permissions.
When a Flask route expects an API key in a header but also accepts an OAuth 2.0 bearer token, developers may inadvertently conflate the two mechanisms. For example, code that reads request.headers.get('Authorization') and passes the value directly to an upstream service or logs it for debugging can leak secrets. If the OAuth 2.0 provider returns user information or internal identifiers in token introspection responses, and those responses are stored or mirrored without filtering, keys or personal data may be exposed in logs or error messages.
Another exposure vector arises from insufficient validation of token provenance. If a Flask app trusts introspection responses without verifying signatures or audience/issuer claims, an attacker who compromises an OAuth 2.0 client or steals a token may use it to infer or retrieve associated API keys stored in backend services. This is especially risky when introspection endpoints are unauthenticated or when caching layers retain sensitive payloads beyond their TTL. Insecure default configurations in OAuth libraries can also expose debug information or stack traces that reference API keys, particularly during development or when error handling is overly verbose.
Middleware or proxy layers in front of Flask may further amplify exposure. For instance, if API gateway logs capture the full Authorization header and those logs are aggregated centrally without masking tokens, any compromise of the logging pipeline leads to direct exposure of bearer tokens and any embedded or associated API keys. Similarly, misconfigured cross-origin resource sharing (CORS) or overly broad route matchers can allow unauthorized origins to trigger introspection calls, increasing the likelihood of token leakage.
To detect these issues, scanners analyze whether OAuth 2.0 flows are implemented with strict validation, whether tokens are masked in logs, and whether introspection responses are sanitized. They also check for unauthenticated introspection endpoints and review how API keys are stored and referenced in the application. In a combined Flask and OAuth 2.0 environment, risk increases when token handling is treated as separate from key management rather than as an integrated control plane.
Oauth2-Specific Remediation in Flask — concrete code fixes
Remediation centers on strict validation, minimal logging, and secure handling of OAuth 2.0 tokens in Flask. Use well-maintained libraries such as authlib or oauthlib and avoid manually parsing or forwarding raw authorization headers. Always validate issuer, audience, expiration, and scopes before using a token to authorize downstream calls. Mask tokens in logs and ensure introspection responses are filtered before storage or transmission.
Below are concrete, working examples for secure OAuth 2.0 usage in Flask.
Validate bearer tokens with Authlib and enforce scopes
from flask import Flask, request, jsonify
from authlib.integrations.flask_oauth2 import ResourceProtector
from authlib.oauth2.rfc6749 import InvalidTokenError
import logging
app = Flask(__name__)
# Configure a validator that checks JWS signatures, audience, and issuer
class BearerTokenValidator:
def __init__(self, public_key_fetch_fn, expected_audience, expected_issuer):
self.public_key_fetch_fn = public_key_fetch_fn
self.expected_audience = expected_audience
self.expected_issuer = expected_issuer
def validate_token(self, token_string):
# fetch public key and validate JWT claims
public_key = self.public_key_fetch_fn(token_string)
claims = decode_jwt(token_string, public_key, audience=self.expected_audience, issuer=self.expected_issuer)
if claims.get('exp') < time.time():
raise InvalidTokenError(description='Token expired')
return claims
validator = BearerTokenValidator(
public_key_fetch_fn=lambda token: fetch_public_key_from_jwks(token),
expected_audience='https://api.example.com',
expected_issuer='https://auth.example.com'
)
require_oauth = ResourceProtector()
require_oauth.register_token_validator(validator)
@app.route('/v1/data')
@require_oauth()
def get_data():
# access token is validated; claims are available via request.oauth_token
token_claims = request.oauth_token
# do not log raw tokens
app.logger.info('Authorized access for sub=%s', token_claims.get('sub'))
return jsonify({ 'status': 'ok', 'user': token_claims.get('sub') })
Secure introspection with filtering and no key echo
import requests
from flask import Flask, request, abort
app = Flask(__name__)
INTROSPECTION_URL = 'https://auth.example.com/introspect'
INTROSPECTION_CLIENT_ID = 'flask-app'
INTROSPECTION_CLIENT_SECRET = 'super-secret'
def introspect_token(token):
resp = requests.post(
INTROSPECTION_URL,
auth=(INTROSPECTION_CLIENT_ID, INTROSPECTION_CLIENT_SECRET),
data={'token': token, 'token_type_hint': 'access_token'},
timeout=5
)
resp.raise_for_status()
data = resp.json()
# filter out sensitive fields before any logging or storage
safe_data = {
'active': data.get('active'),
'scope': data.get('scope'),
'client_id': data.get('client_id')
}
return safe_data
@app.route('/v1/me')
def me():
auth = request.headers.get('Authorization', '')
if not auth.lower().startswith('bearer '):
abort(401, description='Missing bearer token')
token = auth.split(' ', 1)[1]
try:
info = introspect_token(token)
if not info.get('active'):
abort(401, description='Token inactive')
# safe logging: no token or introspection payload echoed
app.logger.info('Token introspection active for client=%s', info.get('client_id'))
return jsonify(info)
except Exception as e:
app.logger.warning('Introspection failed: %s', str(e))
abort(401, description='Invalid token')
Avoid forwarding raw Authorization headers
import requests
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/api/proxy')
def proxy_upstream():
incoming = request.headers.get('Authorization')
if not incoming or not incoming.lower().startswith('bearer '):
abort(400, description='Bearer token required')
# extract and validate instead of forwarding raw header
token = incoming.split(' ', 1)[1]
if not is_token_validated(token):
abort(401, description='Invalid token')
# build a clean downstream header without leaking logs
headers = {'Authorization': f'Bearer {token}'}
# do not log 'headers' directly
resp = requests.get('https://upstream.example.com/resource', headers=headers, timeout=10)
return (resp.content, resp.status_code, resp.headers.items())
Additional best practices: store secrets via environment variables or a secrets manager, enable structured logging with PII/token masking, and regularly rotate credentials. The goal is to treat OAuth 2.0 tokens as sensitive as API keys while avoiding unnecessary propagation and exposure.