secureflow-api-rate-LIMITER 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api_shield/__init__.py +0 -0
- api_shield/auth.py +563 -0
- api_shield/database.py +713 -0
- api_shield/email_notifier.py +154 -0
- api_shield/geo_router.py +551 -0
- api_shield/load_balancer.py +967 -0
- api_shield/log_manager.py +497 -0
- secureflow_api_rate_limiter-1.0.1.dist-info/METADATA +486 -0
- secureflow_api_rate_limiter-1.0.1.dist-info/RECORD +11 -0
- secureflow_api_rate_limiter-1.0.1.dist-info/WHEEL +5 -0
- secureflow_api_rate_limiter-1.0.1.dist-info/top_level.txt +1 -0
api_shield/__init__.py
ADDED
|
File without changes
|
api_shield/auth.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWT Authentication Manager for API Rate Limiter
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive JWT-based authentication and authorization with:
|
|
5
|
+
- Secure token generation with configurable expiration
|
|
6
|
+
- Access token and refresh token management
|
|
7
|
+
- Token verification and validation
|
|
8
|
+
- Token revocation with blacklist support
|
|
9
|
+
- Tier-based authorization decorator
|
|
10
|
+
- Flask route integration for auth endpoints
|
|
11
|
+
- Migration utilities from API key to JWT
|
|
12
|
+
|
|
13
|
+
Security Features:
|
|
14
|
+
- Cryptographically secure token generation
|
|
15
|
+
- Configurable token expiration times
|
|
16
|
+
- Token type validation (access vs refresh)
|
|
17
|
+
- Tier hierarchy enforcement
|
|
18
|
+
- Secure password hashing integration points
|
|
19
|
+
- Token blacklist for revocation
|
|
20
|
+
- Protection against timing attacks
|
|
21
|
+
|
|
22
|
+
Key Components:
|
|
23
|
+
- JWTAuthManager: Main authentication manager class
|
|
24
|
+
- Token generation and verification
|
|
25
|
+
- Flask decorators for endpoint protection
|
|
26
|
+
- Auth endpoints (register, login, refresh, logout)
|
|
27
|
+
- User tier management
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
jwt_manager = JWTAuthManager(secret_key='your-secret-key')
|
|
31
|
+
app.config['JWT_MANAGER'] = jwt_manager
|
|
32
|
+
jwt_manager.init_auth_endpoints(app)
|
|
33
|
+
|
|
34
|
+
@app.route('/protected')
|
|
35
|
+
@jwt_manager.require_jwt(tier_required='premium')
|
|
36
|
+
def protected_route():
|
|
37
|
+
return jsonify({'data': 'protected'})
|
|
38
|
+
|
|
39
|
+
Last Updated: 2026
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import sys
|
|
43
|
+
import base64
|
|
44
|
+
import json
|
|
45
|
+
import hmac
|
|
46
|
+
import time
|
|
47
|
+
import secrets
|
|
48
|
+
import hashlib
|
|
49
|
+
from datetime import datetime, timedelta
|
|
50
|
+
from functools import wraps
|
|
51
|
+
from typing import Dict, Optional, Set
|
|
52
|
+
from threading import Lock
|
|
53
|
+
|
|
54
|
+
from flask import Flask, request, jsonify, current_app
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_free_threaded_python() -> bool:
|
|
58
|
+
abiflags = getattr(sys, "abiflags", "")
|
|
59
|
+
if "t" in abiflags:
|
|
60
|
+
return True
|
|
61
|
+
if hasattr(sys, "_is_gil_enabled"):
|
|
62
|
+
try:
|
|
63
|
+
return not sys._is_gil_enabled() # type: ignore[attr-defined]
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
exe = (sys.executable or "").lower()
|
|
67
|
+
return exe.endswith("t.exe")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class JWTError(Exception):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ExpiredSignatureError(JWTError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class InvalidTokenError(JWTError):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _b64url_encode(data: bytes) -> str:
|
|
83
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _b64url_decode(data: str) -> bytes:
|
|
87
|
+
padding = "=" * (-len(data) % 4)
|
|
88
|
+
return base64.urlsafe_b64decode((data + padding).encode("ascii"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _jwt_json_default(value):
|
|
92
|
+
if isinstance(value, datetime):
|
|
93
|
+
return int(value.timestamp())
|
|
94
|
+
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _jwt_encode_hs256(payload: dict, secret_key: str) -> str:
|
|
98
|
+
header = {"alg": "HS256", "typ": "JWT"}
|
|
99
|
+
header_b64 = _b64url_encode(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
|
100
|
+
payload_b64 = _b64url_encode(
|
|
101
|
+
json.dumps(payload, separators=(",", ":"), default=_jwt_json_default).encode("utf-8")
|
|
102
|
+
)
|
|
103
|
+
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
|
|
104
|
+
sig = hmac.new(secret_key.encode("utf-8"), signing_input, hashlib.sha256).digest()
|
|
105
|
+
return f"{header_b64}.{payload_b64}.{_b64url_encode(sig)}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _jwt_decode_hs256(token: str, secret_key: str) -> dict:
|
|
109
|
+
try:
|
|
110
|
+
header_b64, payload_b64, sig_b64 = token.split(".")
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
raise InvalidTokenError("Invalid JWT format") from e
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
header = json.loads(_b64url_decode(header_b64))
|
|
116
|
+
payload = json.loads(_b64url_decode(payload_b64))
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise InvalidTokenError("Invalid JWT encoding") from e
|
|
119
|
+
|
|
120
|
+
if header.get("alg") != "HS256":
|
|
121
|
+
raise InvalidTokenError("Unsupported JWT algorithm")
|
|
122
|
+
|
|
123
|
+
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
|
|
124
|
+
expected = hmac.new(secret_key.encode("utf-8"), signing_input, hashlib.sha256).digest()
|
|
125
|
+
try:
|
|
126
|
+
given = _b64url_decode(sig_b64)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise InvalidTokenError("Invalid JWT signature encoding") from e
|
|
129
|
+
|
|
130
|
+
if not hmac.compare_digest(expected, given):
|
|
131
|
+
raise InvalidTokenError("Invalid JWT signature")
|
|
132
|
+
|
|
133
|
+
exp = payload.get("exp")
|
|
134
|
+
if exp is not None and int(time.time()) > int(exp):
|
|
135
|
+
raise ExpiredSignatureError("Token expired")
|
|
136
|
+
|
|
137
|
+
return payload
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_PYJWT = None
|
|
141
|
+
if not _is_free_threaded_python():
|
|
142
|
+
try:
|
|
143
|
+
import jwt as _PYJWT # type: ignore
|
|
144
|
+
except Exception:
|
|
145
|
+
_PYJWT = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def jwt_encode(payload: dict, secret_key: str, algorithm: str = "HS256") -> str:
|
|
149
|
+
if algorithm != "HS256":
|
|
150
|
+
raise InvalidTokenError("Only HS256 is supported")
|
|
151
|
+
if _PYJWT is not None:
|
|
152
|
+
return _PYJWT.encode(payload, secret_key, algorithm=algorithm)
|
|
153
|
+
return _jwt_encode_hs256(payload, secret_key)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def jwt_decode(token: str, secret_key: str, algorithms=None) -> dict:
|
|
157
|
+
algorithms = algorithms or ["HS256"]
|
|
158
|
+
if "HS256" not in algorithms:
|
|
159
|
+
raise InvalidTokenError("Only HS256 is supported")
|
|
160
|
+
|
|
161
|
+
if _PYJWT is not None:
|
|
162
|
+
try:
|
|
163
|
+
return _PYJWT.decode(token, secret_key, algorithms=["HS256"])
|
|
164
|
+
except Exception as e:
|
|
165
|
+
name = type(e).__name__
|
|
166
|
+
if name == "ExpiredSignatureError":
|
|
167
|
+
raise ExpiredSignatureError(str(e)) from e
|
|
168
|
+
raise InvalidTokenError(str(e)) from e
|
|
169
|
+
|
|
170
|
+
return _jwt_decode_hs256(token, secret_key)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class JWTAuthManager:
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def __init__(self, secret_key: Optional[str] = None, algorithm: str = 'HS256'):
|
|
179
|
+
"""
|
|
180
|
+
Initialize JWT manager with secure defaults
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
secret_key: Secret key for JWT signing (generates secure random if None)
|
|
184
|
+
algorithm: JWT signing algorithm (default: HS256)
|
|
185
|
+
"""
|
|
186
|
+
# Use provided key or generate secure random key
|
|
187
|
+
self.secret_key = secret_key or secrets.token_urlsafe(32)
|
|
188
|
+
self.algorithm = algorithm
|
|
189
|
+
|
|
190
|
+
# Validate secret key strength
|
|
191
|
+
if len(self.secret_key) < 32:
|
|
192
|
+
raise ValueError("Secret key must be at least 32 characters for security")
|
|
193
|
+
|
|
194
|
+
# Token expiration times in seconds
|
|
195
|
+
self.access_token_expiry = 3600 # 1 hour
|
|
196
|
+
self.refresh_token_expiry = 604800 # 7 days (not 600 seconds!)
|
|
197
|
+
|
|
198
|
+
# Token blacklist for revocation (in production, use Redis)
|
|
199
|
+
self._token_blacklist: Set[str] = set()
|
|
200
|
+
self._blacklist_lock = Lock()
|
|
201
|
+
|
|
202
|
+
# Security logging
|
|
203
|
+
print(f"✅ JWT Manager initialized")
|
|
204
|
+
print(f"🔐 Algorithm: {self.algorithm}")
|
|
205
|
+
print(f"⏱️ Refresh token expiry: {self.refresh_token_expiry}s")
|
|
206
|
+
|
|
207
|
+
def generate_tokens(
|
|
208
|
+
self,
|
|
209
|
+
user_id: str,
|
|
210
|
+
tier: str = 'free',
|
|
211
|
+
metadata: Optional[Dict] = None
|
|
212
|
+
) -> Dict:
|
|
213
|
+
|
|
214
|
+
# Validate inputs
|
|
215
|
+
if not user_id or not isinstance(user_id, str):
|
|
216
|
+
raise ValueError("user_id must be a non-empty string")
|
|
217
|
+
|
|
218
|
+
valid_tiers = ['free', 'basic', 'premium', 'enterprise']
|
|
219
|
+
if tier not in valid_tiers:
|
|
220
|
+
raise ValueError(f"tier must be one of {valid_tiers}")
|
|
221
|
+
|
|
222
|
+
now_ts = int(time.time())
|
|
223
|
+
|
|
224
|
+
# Access token payload
|
|
225
|
+
access_payload = {
|
|
226
|
+
'user_id': user_id,
|
|
227
|
+
'tier': tier,
|
|
228
|
+
'iat': now_ts, # Issued at
|
|
229
|
+
'exp': now_ts + self.access_token_expiry,
|
|
230
|
+
'type': 'access',
|
|
231
|
+
'metadata': metadata or {},
|
|
232
|
+
'jti': secrets.token_hex(16), # Unique token ID for revocation
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Refresh token payload (minimal data for security)
|
|
236
|
+
refresh_payload = {
|
|
237
|
+
'user_id': user_id,
|
|
238
|
+
'iat': now_ts,
|
|
239
|
+
'exp': now_ts + self.refresh_token_expiry,
|
|
240
|
+
'type': 'refresh',
|
|
241
|
+
'jti': secrets.token_hex(16),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Generate tokens
|
|
245
|
+
access_token = jwt_encode(access_payload, self.secret_key, algorithm=self.algorithm)
|
|
246
|
+
|
|
247
|
+
refresh_token = jwt_encode(refresh_payload, self.secret_key, algorithm=self.algorithm)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
'access_token': access_token,
|
|
251
|
+
'refresh_token': refresh_token,
|
|
252
|
+
'refresh_token_expiry': self.refresh_token_expiry,
|
|
253
|
+
'token_type': 'Bearer',
|
|
254
|
+
'user_id': user_id,
|
|
255
|
+
'tier': tier,
|
|
256
|
+
'metadata': metadata or {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
def verify_token(self, token: str, token_type: str = 'access') -> Dict:
|
|
260
|
+
|
|
261
|
+
if not token or not isinstance(token, str):
|
|
262
|
+
raise InvalidTokenError("Token must be a non-empty string")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
# Decode and verify token
|
|
266
|
+
payload = jwt_decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
267
|
+
|
|
268
|
+
# Verify token type matches expected
|
|
269
|
+
if payload.get('type') != token_type:
|
|
270
|
+
raise InvalidTokenError(
|
|
271
|
+
f"Invalid token type. Expected '{token_type}', got '{payload.get('type')}'"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Check if token is blacklisted (revoked)
|
|
275
|
+
jti = payload.get('jti')
|
|
276
|
+
if jti and self._is_token_blacklisted(jti):
|
|
277
|
+
raise ValueError(f"Token has been revoked")
|
|
278
|
+
|
|
279
|
+
return payload
|
|
280
|
+
|
|
281
|
+
except ExpiredSignatureError as e:
|
|
282
|
+
raise ExpiredSignatureError(f"Token expired: {str(e)}")
|
|
283
|
+
except InvalidTokenError as e:
|
|
284
|
+
raise InvalidTokenError(f"Invalid token: {str(e)}")
|
|
285
|
+
|
|
286
|
+
def refresh_access_token(self, refresh_token: str) -> Dict:
|
|
287
|
+
|
|
288
|
+
# Verify refresh token
|
|
289
|
+
payload = self.verify_token(refresh_token, token_type='refresh')
|
|
290
|
+
user_id = payload.get('user_id')
|
|
291
|
+
|
|
292
|
+
if not user_id:
|
|
293
|
+
raise InvalidTokenError("Refresh token missing user_id")
|
|
294
|
+
|
|
295
|
+
# Generate new token pair (preserve tier from original if stored in DB)
|
|
296
|
+
# In production, fetch user tier from database
|
|
297
|
+
return self.generate_tokens(user_id, tier='free')
|
|
298
|
+
|
|
299
|
+
def revoke_token(self, token: str) -> None:
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
payload = self.verify_token(token)
|
|
303
|
+
jti = payload.get('jti')
|
|
304
|
+
|
|
305
|
+
if jti:
|
|
306
|
+
with self._blacklist_lock:
|
|
307
|
+
self._token_blacklist.add(jti)
|
|
308
|
+
print(f"✅ Token revoked: {jti}")
|
|
309
|
+
else:
|
|
310
|
+
print(f"⚠️ Token has no JTI, cannot revoke reliably")
|
|
311
|
+
|
|
312
|
+
except (InvalidTokenError, ExpiredSignatureError) as e:
|
|
313
|
+
print(f"⚠️ Cannot revoke invalid/expired token: {str(e)}")
|
|
314
|
+
|
|
315
|
+
def _is_token_blacklisted(self, jti: str) -> bool:
|
|
316
|
+
"""Check if token JTI is in blacklist"""
|
|
317
|
+
with self._blacklist_lock:
|
|
318
|
+
return jti in self._token_blacklist
|
|
319
|
+
|
|
320
|
+
def clear_blacklist(self) -> None:
|
|
321
|
+
"""Clear token blacklist (admin function)"""
|
|
322
|
+
with self._blacklist_lock:
|
|
323
|
+
count = len(self._token_blacklist)
|
|
324
|
+
self._token_blacklist.clear()
|
|
325
|
+
print(f"🗑️ Cleared {count} tokens from blacklist")
|
|
326
|
+
|
|
327
|
+
def require_jwt(self, tier_required: Optional[str] = None):
|
|
328
|
+
|
|
329
|
+
def decorator(func):
|
|
330
|
+
@wraps(func)
|
|
331
|
+
def wrapper(*args, **kwargs):
|
|
332
|
+
# Extract token from Authorization header
|
|
333
|
+
auth_header = request.headers.get('Authorization', '')
|
|
334
|
+
|
|
335
|
+
if not auth_header or not auth_header.startswith('Bearer '):
|
|
336
|
+
return jsonify({
|
|
337
|
+
'error': 'Unauthorized',
|
|
338
|
+
'message': 'Missing or invalid Authorization header',
|
|
339
|
+
'format': 'Authorization: Bearer <token>'
|
|
340
|
+
}), 401
|
|
341
|
+
|
|
342
|
+
# Parse token from header
|
|
343
|
+
parts = auth_header.split()
|
|
344
|
+
if len(parts) != 2:
|
|
345
|
+
return jsonify({
|
|
346
|
+
'error': 'Unauthorized',
|
|
347
|
+
'message': 'Invalid Authorization header format',
|
|
348
|
+
'format': 'Authorization: Bearer <token>'
|
|
349
|
+
}), 401
|
|
350
|
+
|
|
351
|
+
token = parts[1]
|
|
352
|
+
|
|
353
|
+
# Get JWT manager from app config
|
|
354
|
+
jwt_manager = current_app.config.get('JWT_MANAGER')
|
|
355
|
+
if not jwt_manager:
|
|
356
|
+
return jsonify({
|
|
357
|
+
'error': 'Server configuration error',
|
|
358
|
+
'message': 'JWT manager not configured'
|
|
359
|
+
}), 500
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
# Verify token
|
|
363
|
+
payload = jwt_manager.verify_token(token)
|
|
364
|
+
user_tier = payload.get('tier', 'free')
|
|
365
|
+
|
|
366
|
+
# Check tier requirement if specified
|
|
367
|
+
if tier_required:
|
|
368
|
+
tier_hierarchy = ['free', 'basic', 'premium', 'enterprise']
|
|
369
|
+
|
|
370
|
+
if tier_required not in tier_hierarchy:
|
|
371
|
+
return jsonify({
|
|
372
|
+
'error': 'Server configuration error',
|
|
373
|
+
'message': f'Invalid tier requirement: {tier_required}'
|
|
374
|
+
}), 500
|
|
375
|
+
|
|
376
|
+
# Check if user tier meets requirement
|
|
377
|
+
if (tier_hierarchy.index(user_tier) <
|
|
378
|
+
tier_hierarchy.index(tier_required)):
|
|
379
|
+
return jsonify({
|
|
380
|
+
'error': 'Insufficient tier',
|
|
381
|
+
'message': f'This endpoint requires {tier_required} tier or higher',
|
|
382
|
+
'required': tier_required,
|
|
383
|
+
'current': user_tier
|
|
384
|
+
}), 403
|
|
385
|
+
|
|
386
|
+
# Attach user info to request context
|
|
387
|
+
request.user_id = payload['user_id']
|
|
388
|
+
request.user_tier = user_tier
|
|
389
|
+
request.jwt_payload = payload
|
|
390
|
+
|
|
391
|
+
return func(*args, **kwargs)
|
|
392
|
+
|
|
393
|
+
except ExpiredSignatureError:
|
|
394
|
+
return jsonify({
|
|
395
|
+
'error': 'Token expired',
|
|
396
|
+
'message': 'Your token has expired. Please refresh or login again.'
|
|
397
|
+
}), 401
|
|
398
|
+
|
|
399
|
+
except InvalidTokenError as e:
|
|
400
|
+
return jsonify({
|
|
401
|
+
'error': 'Invalid token',
|
|
402
|
+
'message': str(e)
|
|
403
|
+
}), 401
|
|
404
|
+
|
|
405
|
+
except ValueError as e:
|
|
406
|
+
return jsonify({
|
|
407
|
+
'error': 'Token revoked',
|
|
408
|
+
'message': str(e)
|
|
409
|
+
}), 401
|
|
410
|
+
|
|
411
|
+
return wrapper
|
|
412
|
+
return decorator
|
|
413
|
+
|
|
414
|
+
def init_auth_endpoints(self, app: Flask) -> None:
|
|
415
|
+
|
|
416
|
+
@app.route('/auth/register', methods=['POST'])
|
|
417
|
+
def register():
|
|
418
|
+
|
|
419
|
+
data = request.get_json()
|
|
420
|
+
|
|
421
|
+
if not data:
|
|
422
|
+
return jsonify({'error': 'JSON body required'}), 400
|
|
423
|
+
|
|
424
|
+
email = data.get('email')
|
|
425
|
+
password = data.get('password')
|
|
426
|
+
tier = data.get('tier', 'free')
|
|
427
|
+
|
|
428
|
+
if not email or not isinstance(email, str):
|
|
429
|
+
return jsonify({'error': 'Valid email required'}), 400
|
|
430
|
+
|
|
431
|
+
if not password or not isinstance(password, str):
|
|
432
|
+
return jsonify({'error': 'Valid password required'}), 400
|
|
433
|
+
|
|
434
|
+
if len(password) < 8:
|
|
435
|
+
return jsonify({'error': 'Password must be at least 8 characters'}), 400
|
|
436
|
+
|
|
437
|
+
valid_tiers = ['free', 'basic', 'premium', 'enterprise']
|
|
438
|
+
if tier not in valid_tiers:
|
|
439
|
+
return jsonify({'error': f'Tier must be one of {valid_tiers}'}), 400
|
|
440
|
+
|
|
441
|
+
user_id = hashlib.sha256(email.encode('utf-8')).hexdigest()[:16]
|
|
442
|
+
|
|
443
|
+
jwt_manager = current_app.config.get('JWT_MANAGER')
|
|
444
|
+
if not jwt_manager:
|
|
445
|
+
return jsonify({'error': 'Server configuration error'}), 500
|
|
446
|
+
|
|
447
|
+
tokens = jwt_manager.generate_tokens(
|
|
448
|
+
user_id=user_id,
|
|
449
|
+
tier=tier,
|
|
450
|
+
metadata={'email': email}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return jsonify(tokens), 201
|
|
454
|
+
|
|
455
|
+
@app.route('/auth/login', methods=['POST'])
|
|
456
|
+
def login():
|
|
457
|
+
"""Login user and return tokens"""
|
|
458
|
+
data = request.get_json()
|
|
459
|
+
|
|
460
|
+
if not data:
|
|
461
|
+
return jsonify({'error': 'JSON body required'}), 400
|
|
462
|
+
|
|
463
|
+
email = data.get('email')
|
|
464
|
+
password = data.get('password')
|
|
465
|
+
|
|
466
|
+
if not email or not password:
|
|
467
|
+
return jsonify({'error': 'Email and password required'}), 400
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
user_id = hashlib.sha256(email.encode('utf-8')).hexdigest()[:16]
|
|
471
|
+
|
|
472
|
+
jwt_manager = current_app.config.get('JWT_MANAGER')
|
|
473
|
+
if not jwt_manager:
|
|
474
|
+
return jsonify({'error': 'Server configuration error'}), 500
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
tokens = jwt_manager.generate_tokens(
|
|
478
|
+
user_id=user_id,
|
|
479
|
+
tier='free',
|
|
480
|
+
metadata={'email': email}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
return jsonify(tokens)
|
|
484
|
+
|
|
485
|
+
@app.route('/auth/refresh', methods=['POST'])
|
|
486
|
+
def refresh():
|
|
487
|
+
"""Refresh access token using refresh token"""
|
|
488
|
+
data = request.get_json()
|
|
489
|
+
|
|
490
|
+
if not data:
|
|
491
|
+
return jsonify({'error': 'JSON body required'}), 400
|
|
492
|
+
|
|
493
|
+
refresh_token = data.get('refresh_token')
|
|
494
|
+
|
|
495
|
+
if not refresh_token:
|
|
496
|
+
return jsonify({'error': 'Refresh token required'}), 400
|
|
497
|
+
|
|
498
|
+
# Get JWT manager
|
|
499
|
+
jwt_manager = current_app.config.get('JWT_MANAGER')
|
|
500
|
+
if not jwt_manager:
|
|
501
|
+
return jsonify({'error': 'Server configuration error'}), 500
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
tokens = jwt_manager.refresh_access_token(refresh_token)
|
|
505
|
+
return jsonify(tokens)
|
|
506
|
+
except (InvalidTokenError, ExpiredSignatureError, ValueError) as e:
|
|
507
|
+
return jsonify({'error': str(e)}), 401
|
|
508
|
+
|
|
509
|
+
@app.route('/auth/logout', methods=['POST'])
|
|
510
|
+
@self.require_jwt()
|
|
511
|
+
def logout():
|
|
512
|
+
"""Logout user and revoke token"""
|
|
513
|
+
auth_header = request.headers.get('Authorization', '')
|
|
514
|
+
token = auth_header.split()[1]
|
|
515
|
+
|
|
516
|
+
# Get JWT manager
|
|
517
|
+
jwt_manager = current_app.config.get('JWT_MANAGER')
|
|
518
|
+
if not jwt_manager:
|
|
519
|
+
return jsonify({'error': 'Server configuration error'}), 500
|
|
520
|
+
|
|
521
|
+
jwt_manager.revoke_token(token)
|
|
522
|
+
|
|
523
|
+
return jsonify({'message': 'Logged out successfully'})
|
|
524
|
+
|
|
525
|
+
@app.route('/auth/me', methods=['GET'])
|
|
526
|
+
@self.require_jwt()
|
|
527
|
+
def get_user_info():
|
|
528
|
+
"""Get current user information"""
|
|
529
|
+
return jsonify({
|
|
530
|
+
'user_id': request.user_id,
|
|
531
|
+
'tier': request.user_tier,
|
|
532
|
+
'token_info': request.jwt_payload
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
print("✅ Auth endpoints initialized:")
|
|
536
|
+
print(" POST /auth/register - Register new user")
|
|
537
|
+
print(" POST /auth/login - Login and get tokens")
|
|
538
|
+
print(" POST /auth/refresh - Refresh access token")
|
|
539
|
+
print(" POST /auth/logout - Logout and revoke token")
|
|
540
|
+
print(" GET /auth/me - Get current user info")
|
|
541
|
+
|
|
542
|
+
def migrate_api_key_to_jwt(
|
|
543
|
+
api_key: str,
|
|
544
|
+
jwt_manager: JWTAuthManager,
|
|
545
|
+
tier: str = 'free'
|
|
546
|
+
) -> Dict:
|
|
547
|
+
|
|
548
|
+
if not api_key or not isinstance(api_key, str):
|
|
549
|
+
raise ValueError("api_key must be a non-empty string")
|
|
550
|
+
|
|
551
|
+
# Generate deterministic user_id from API key
|
|
552
|
+
user_id = hashlib.sha256(api_key.encode('utf-8')).hexdigest()[:16]
|
|
553
|
+
|
|
554
|
+
# Generate JWT tokens
|
|
555
|
+
return jwt_manager.generate_tokens(user_id, tier)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def create_jwt_manager(app: Flask, secret_key: Optional[str] = None) -> JWTAuthManager:
|
|
559
|
+
|
|
560
|
+
jwt_manager = JWTAuthManager(secret_key=secret_key)
|
|
561
|
+
app.config['JWT_MANAGER'] = jwt_manager
|
|
562
|
+
jwt_manager.init_auth_endpoints(app)
|
|
563
|
+
return jwt_manager
|