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