api-mocker 0.4.0__py3-none-any.whl → 0.5.0__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.
@@ -0,0 +1,610 @@
1
+ """
2
+ Advanced Authentication System
3
+
4
+ This module provides comprehensive authentication capabilities including:
5
+ - OAuth2 with multiple providers (Google, GitHub, Microsoft, etc.)
6
+ - JWT token management with refresh tokens
7
+ - API key authentication
8
+ - Role-based access control (RBAC)
9
+ - Multi-factor authentication (MFA)
10
+ - Session management
11
+ - Password policies and validation
12
+ """
13
+
14
+ import jwt
15
+ import hashlib
16
+ import secrets
17
+ import time
18
+ from typing import Any, Dict, List, Optional, Union, Callable
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ from datetime import datetime, timedelta
22
+ import json
23
+ import base64
24
+ import hmac
25
+ import pyotp
26
+ import qrcode
27
+ from io import BytesIO
28
+
29
+
30
+ class AuthProvider(Enum):
31
+ """Authentication providers"""
32
+ LOCAL = "local"
33
+ GOOGLE = "google"
34
+ GITHUB = "github"
35
+ MICROSOFT = "microsoft"
36
+ FACEBOOK = "facebook"
37
+ TWITTER = "twitter"
38
+ LINKEDIN = "linkedin"
39
+ DISCORD = "discord"
40
+
41
+
42
+ class TokenType(Enum):
43
+ """Token types"""
44
+ ACCESS = "access"
45
+ REFRESH = "refresh"
46
+ API_KEY = "api_key"
47
+ MFA = "mfa"
48
+
49
+
50
+ class UserRole(Enum):
51
+ """User roles"""
52
+ ADMIN = "admin"
53
+ USER = "user"
54
+ MODERATOR = "moderator"
55
+ GUEST = "guest"
56
+ API_USER = "api_user"
57
+
58
+
59
+ @dataclass
60
+ class User:
61
+ """User model"""
62
+ id: str
63
+ username: str
64
+ email: str
65
+ password_hash: str
66
+ roles: List[UserRole] = field(default_factory=lambda: [UserRole.USER])
67
+ is_active: bool = True
68
+ is_verified: bool = False
69
+ created_at: datetime = field(default_factory=datetime.now)
70
+ last_login: Optional[datetime] = None
71
+ mfa_secret: Optional[str] = None
72
+ mfa_enabled: bool = False
73
+ metadata: Dict[str, Any] = field(default_factory=dict)
74
+
75
+
76
+ @dataclass
77
+ class Token:
78
+ """Token model"""
79
+ token: str
80
+ token_type: TokenType
81
+ user_id: str
82
+ expires_at: datetime
83
+ created_at: datetime = field(default_factory=datetime.now)
84
+ is_revoked: bool = False
85
+ metadata: Dict[str, Any] = field(default_factory=dict)
86
+
87
+
88
+ @dataclass
89
+ class APIKey:
90
+ """API Key model"""
91
+ key: str
92
+ name: str
93
+ user_id: str
94
+ permissions: List[str] = field(default_factory=list)
95
+ rate_limit: Optional[int] = None
96
+ expires_at: Optional[datetime] = None
97
+ is_active: bool = True
98
+ created_at: datetime = field(default_factory=datetime.now)
99
+ last_used: Optional[datetime] = None
100
+ usage_count: int = 0
101
+
102
+
103
+ @dataclass
104
+ class OAuth2Provider:
105
+ """OAuth2 provider configuration"""
106
+ name: AuthProvider
107
+ client_id: str
108
+ client_secret: str
109
+ authorization_url: str
110
+ token_url: str
111
+ user_info_url: str
112
+ scope: List[str] = field(default_factory=list)
113
+ redirect_uri: str = ""
114
+
115
+
116
+ @dataclass
117
+ class AuthSession:
118
+ """Authentication session"""
119
+ session_id: str
120
+ user_id: str
121
+ ip_address: str
122
+ user_agent: str
123
+ created_at: datetime = field(default_factory=datetime.now)
124
+ last_activity: datetime = field(default_factory=datetime.now)
125
+ expires_at: datetime = field(default_factory=lambda: datetime.now() + timedelta(hours=24))
126
+ is_active: bool = True
127
+
128
+
129
+ class PasswordValidator:
130
+ """Password validation utility"""
131
+
132
+ def __init__(self, min_length: int = 8, require_uppercase: bool = True,
133
+ require_lowercase: bool = True, require_numbers: bool = True,
134
+ require_special: bool = True):
135
+ self.min_length = min_length
136
+ self.require_uppercase = require_uppercase
137
+ self.require_lowercase = require_lowercase
138
+ self.require_numbers = require_numbers
139
+ self.require_special = require_special
140
+
141
+ def validate(self, password: str) -> Dict[str, Any]:
142
+ """Validate password and return validation result"""
143
+ errors = []
144
+
145
+ if len(password) < self.min_length:
146
+ errors.append(f"Password must be at least {self.min_length} characters long")
147
+
148
+ if self.require_uppercase and not any(c.isupper() for c in password):
149
+ errors.append("Password must contain at least one uppercase letter")
150
+
151
+ if self.require_lowercase and not any(c.islower() for c in password):
152
+ errors.append("Password must contain at least one lowercase letter")
153
+
154
+ if self.require_numbers and not any(c.isdigit() for c in password):
155
+ errors.append("Password must contain at least one number")
156
+
157
+ if self.require_special and not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
158
+ errors.append("Password must contain at least one special character")
159
+
160
+ return {
161
+ "is_valid": len(errors) == 0,
162
+ "errors": errors,
163
+ "strength": self._calculate_strength(password)
164
+ }
165
+
166
+ def _calculate_strength(self, password: str) -> str:
167
+ """Calculate password strength"""
168
+ score = 0
169
+
170
+ if len(password) >= 8:
171
+ score += 1
172
+ if len(password) >= 12:
173
+ score += 1
174
+ if any(c.isupper() for c in password):
175
+ score += 1
176
+ if any(c.islower() for c in password):
177
+ score += 1
178
+ if any(c.isdigit() for c in password):
179
+ score += 1
180
+ if any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
181
+ score += 1
182
+
183
+ if score <= 2:
184
+ return "weak"
185
+ elif score <= 4:
186
+ return "medium"
187
+ else:
188
+ return "strong"
189
+
190
+
191
+ class JWTManager:
192
+ """JWT token management"""
193
+
194
+ def __init__(self, secret_key: str, algorithm: str = "HS256"):
195
+ self.secret_key = secret_key
196
+ self.algorithm = algorithm
197
+
198
+ def create_token(self, user_id: str, token_type: TokenType,
199
+ expires_in: int = 3600, **kwargs) -> str:
200
+ """Create a JWT token"""
201
+ now = datetime.utcnow()
202
+ payload = {
203
+ "user_id": user_id,
204
+ "token_type": token_type.value,
205
+ "iat": now,
206
+ "exp": now + timedelta(seconds=expires_in),
207
+ **kwargs
208
+ }
209
+ return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
210
+
211
+ def verify_token(self, token: str) -> Dict[str, Any]:
212
+ """Verify and decode a JWT token"""
213
+ try:
214
+ payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
215
+ return {"valid": True, "payload": payload}
216
+ except jwt.ExpiredSignatureError:
217
+ return {"valid": False, "error": "Token expired"}
218
+ except jwt.InvalidTokenError:
219
+ return {"valid": False, "error": "Invalid token"}
220
+
221
+ def refresh_token(self, refresh_token: str) -> Optional[str]:
222
+ """Refresh an access token using refresh token"""
223
+ result = self.verify_token(refresh_token)
224
+ if result["valid"] and result["payload"]["token_type"] == "refresh":
225
+ user_id = result["payload"]["user_id"]
226
+ return self.create_token(user_id, TokenType.ACCESS)
227
+ return None
228
+
229
+
230
+ class MFAHandler:
231
+ """Multi-factor authentication handler"""
232
+
233
+ def __init__(self):
234
+ self.totp = pyotp.TOTP
235
+
236
+ def generate_secret(self) -> str:
237
+ """Generate a TOTP secret"""
238
+ return pyotp.random_base32()
239
+
240
+ def generate_qr_code(self, user_email: str, secret: str, issuer: str = "API-Mocker") -> str:
241
+ """Generate QR code for MFA setup"""
242
+ totp_uri = pyotp.totp.TOTP(secret).provisioning_uri(
243
+ name=user_email,
244
+ issuer_name=issuer
245
+ )
246
+ return totp_uri
247
+
248
+ def verify_code(self, secret: str, code: str) -> bool:
249
+ """Verify TOTP code"""
250
+ totp = pyotp.TOTP(secret)
251
+ return totp.verify(code, valid_window=1)
252
+
253
+ def generate_backup_codes(self, count: int = 10) -> List[str]:
254
+ """Generate backup codes for MFA"""
255
+ return [secrets.token_hex(4).upper() for _ in range(count)]
256
+
257
+
258
+ class AdvancedAuthSystem:
259
+ """Main authentication system"""
260
+
261
+ def __init__(self, secret_key: str = None):
262
+ self.secret_key = secret_key or secrets.token_hex(32)
263
+ self.jwt_manager = JWTManager(self.secret_key)
264
+ self.mfa_handler = MFAHandler()
265
+ self.password_validator = PasswordValidator()
266
+
267
+ # Storage
268
+ self.users: Dict[str, User] = {}
269
+ self.tokens: Dict[str, Token] = {}
270
+ self.api_keys: Dict[str, APIKey] = {}
271
+ self.sessions: Dict[str, AuthSession] = {}
272
+ self.oauth_providers: Dict[AuthProvider, OAuth2Provider] = {}
273
+
274
+ # Rate limiting
275
+ self.login_attempts: Dict[str, List[datetime]] = {}
276
+ self.max_login_attempts = 5
277
+ self.lockout_duration = 300 # 5 minutes
278
+
279
+ def hash_password(self, password: str) -> str:
280
+ """Hash a password using PBKDF2"""
281
+ salt = secrets.token_hex(16)
282
+ pwd_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
283
+ return f"{salt}:{pwd_hash.hex()}"
284
+
285
+ def verify_password(self, password: str, password_hash: str) -> bool:
286
+ """Verify a password against its hash"""
287
+ try:
288
+ salt, hash_hex = password_hash.split(':')
289
+ pwd_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
290
+ return hmac.compare_digest(hash_hex, pwd_hash.hex())
291
+ except ValueError:
292
+ return False
293
+
294
+ def register_user(self, username: str, email: str, password: str,
295
+ roles: List[UserRole] = None) -> Dict[str, Any]:
296
+ """Register a new user"""
297
+ # Validate password
298
+ password_validation = self.password_validator.validate(password)
299
+ if not password_validation["is_valid"]:
300
+ return {
301
+ "success": False,
302
+ "errors": password_validation["errors"]
303
+ }
304
+
305
+ # Check if user already exists
306
+ if any(user.email == email for user in self.users.values()):
307
+ return {"success": False, "error": "Email already registered"}
308
+
309
+ if any(user.username == username for user in self.users.values()):
310
+ return {"success": False, "error": "Username already taken"}
311
+
312
+ # Create user
313
+ user_id = secrets.token_hex(16)
314
+ user = User(
315
+ id=user_id,
316
+ username=username,
317
+ email=email,
318
+ password_hash=self.hash_password(password),
319
+ roles=roles or [UserRole.USER]
320
+ )
321
+
322
+ self.users[user_id] = user
323
+
324
+ return {
325
+ "success": True,
326
+ "user_id": user_id,
327
+ "message": "User registered successfully"
328
+ }
329
+
330
+ def authenticate_user(self, email: str, password: str,
331
+ ip_address: str = None, user_agent: str = None) -> Dict[str, Any]:
332
+ """Authenticate a user with email and password"""
333
+ # Check rate limiting
334
+ if self._is_rate_limited(email):
335
+ return {"success": False, "error": "Too many login attempts"}
336
+
337
+ # Find user
338
+ user = None
339
+ for u in self.users.values():
340
+ if u.email == email:
341
+ user = u
342
+ break
343
+
344
+ if not user:
345
+ self._record_failed_attempt(email)
346
+ return {"success": False, "error": "Invalid credentials"}
347
+
348
+ # Verify password
349
+ if not self.verify_password(password, user.password_hash):
350
+ self._record_failed_attempt(email)
351
+ return {"success": False, "error": "Invalid credentials"}
352
+
353
+ # Check if user is active
354
+ if not user.is_active:
355
+ return {"success": False, "error": "Account is disabled"}
356
+
357
+ # Update last login
358
+ user.last_login = datetime.now()
359
+
360
+ # Create tokens
361
+ access_token = self.jwt_manager.create_token(
362
+ user.id, TokenType.ACCESS, expires_in=3600
363
+ )
364
+ refresh_token = self.jwt_manager.create_token(
365
+ user.id, TokenType.REFRESH, expires_in=86400 * 7 # 7 days
366
+ )
367
+
368
+ # Create session
369
+ session_id = secrets.token_hex(32)
370
+ session = AuthSession(
371
+ session_id=session_id,
372
+ user_id=user.id,
373
+ ip_address=ip_address or "unknown",
374
+ user_agent=user_agent or "unknown"
375
+ )
376
+ self.sessions[session_id] = session
377
+
378
+ # Clear failed attempts
379
+ if email in self.login_attempts:
380
+ del self.login_attempts[email]
381
+
382
+ return {
383
+ "success": True,
384
+ "access_token": access_token,
385
+ "refresh_token": refresh_token,
386
+ "session_id": session_id,
387
+ "user": {
388
+ "id": user.id,
389
+ "username": user.username,
390
+ "email": user.email,
391
+ "roles": [role.value for role in user.roles],
392
+ "mfa_enabled": user.mfa_enabled
393
+ }
394
+ }
395
+
396
+ def verify_token(self, token: str) -> Dict[str, Any]:
397
+ """Verify a JWT token"""
398
+ result = self.jwt_manager.verify_token(token)
399
+ if not result["valid"]:
400
+ return result
401
+
402
+ payload = result["payload"]
403
+ user_id = payload.get("user_id")
404
+
405
+ if user_id not in self.users:
406
+ return {"valid": False, "error": "User not found"}
407
+
408
+ user = self.users[user_id]
409
+ if not user.is_active:
410
+ return {"valid": False, "error": "User account is disabled"}
411
+
412
+ return {
413
+ "valid": True,
414
+ "user_id": user_id,
415
+ "payload": payload
416
+ }
417
+
418
+ def create_api_key(self, user_id: str, name: str, permissions: List[str] = None,
419
+ rate_limit: int = None, expires_in: int = None) -> Dict[str, Any]:
420
+ """Create an API key for a user"""
421
+ if user_id not in self.users:
422
+ return {"success": False, "error": "User not found"}
423
+
424
+ # Generate API key
425
+ api_key = f"ak_{secrets.token_hex(32)}"
426
+
427
+ # Set expiration
428
+ expires_at = None
429
+ if expires_in:
430
+ expires_at = datetime.now() + timedelta(seconds=expires_in)
431
+
432
+ key_obj = APIKey(
433
+ key=api_key,
434
+ name=name,
435
+ user_id=user_id,
436
+ permissions=permissions or [],
437
+ rate_limit=rate_limit,
438
+ expires_at=expires_at
439
+ )
440
+
441
+ self.api_keys[api_key] = key_obj
442
+
443
+ return {
444
+ "success": True,
445
+ "api_key": api_key,
446
+ "expires_at": expires_at.isoformat() if expires_at else None
447
+ }
448
+
449
+ def verify_api_key(self, api_key: str) -> Dict[str, Any]:
450
+ """Verify an API key"""
451
+ if api_key not in self.api_keys:
452
+ return {"valid": False, "error": "Invalid API key"}
453
+
454
+ key_obj = self.api_keys[api_key]
455
+
456
+ if not key_obj.is_active:
457
+ return {"valid": False, "error": "API key is disabled"}
458
+
459
+ if key_obj.expires_at and datetime.now() > key_obj.expires_at:
460
+ return {"valid": False, "error": "API key expired"}
461
+
462
+ # Update usage
463
+ key_obj.last_used = datetime.now()
464
+ key_obj.usage_count += 1
465
+
466
+ return {
467
+ "valid": True,
468
+ "user_id": key_obj.user_id,
469
+ "permissions": key_obj.permissions,
470
+ "rate_limit": key_obj.rate_limit
471
+ }
472
+
473
+ def setup_mfa(self, user_id: str) -> Dict[str, Any]:
474
+ """Setup MFA for a user"""
475
+ if user_id not in self.users:
476
+ return {"success": False, "error": "User not found"}
477
+
478
+ user = self.users[user_id]
479
+ secret = self.mfa_handler.generate_secret()
480
+ user.mfa_secret = secret
481
+
482
+ qr_code_uri = self.mfa_handler.generate_qr_code(user.email, secret)
483
+ backup_codes = self.mfa_handler.generate_backup_codes()
484
+
485
+ return {
486
+ "success": True,
487
+ "secret": secret,
488
+ "qr_code_uri": qr_code_uri,
489
+ "backup_codes": backup_codes
490
+ }
491
+
492
+ def enable_mfa(self, user_id: str, code: str) -> Dict[str, Any]:
493
+ """Enable MFA for a user"""
494
+ if user_id not in self.users:
495
+ return {"success": False, "error": "User not found"}
496
+
497
+ user = self.users[user_id]
498
+ if not user.mfa_secret:
499
+ return {"success": False, "error": "MFA not set up"}
500
+
501
+ if not self.mfa_handler.verify_code(user.mfa_secret, code):
502
+ return {"success": False, "error": "Invalid MFA code"}
503
+
504
+ user.mfa_enabled = True
505
+ return {"success": True, "message": "MFA enabled successfully"}
506
+
507
+ def verify_mfa(self, user_id: str, code: str) -> bool:
508
+ """Verify MFA code"""
509
+ if user_id not in self.users:
510
+ return False
511
+
512
+ user = self.users[user_id]
513
+ if not user.mfa_enabled or not user.mfa_secret:
514
+ return False
515
+
516
+ return self.mfa_handler.verify_code(user.mfa_secret, code)
517
+
518
+ def _is_rate_limited(self, email: str) -> bool:
519
+ """Check if email is rate limited"""
520
+ if email not in self.login_attempts:
521
+ return False
522
+
523
+ now = datetime.now()
524
+ attempts = self.login_attempts[email]
525
+
526
+ # Remove old attempts
527
+ attempts = [attempt for attempt in attempts if now - attempt < timedelta(seconds=self.lockout_duration)]
528
+ self.login_attempts[email] = attempts
529
+
530
+ return len(attempts) >= self.max_login_attempts
531
+
532
+ def _record_failed_attempt(self, email: str) -> None:
533
+ """Record a failed login attempt"""
534
+ if email not in self.login_attempts:
535
+ self.login_attempts[email] = []
536
+
537
+ self.login_attempts[email].append(datetime.now())
538
+
539
+ def add_oauth_provider(self, provider: OAuth2Provider) -> None:
540
+ """Add an OAuth2 provider"""
541
+ self.oauth_providers[provider.name] = provider
542
+
543
+ def get_oauth_authorization_url(self, provider: AuthProvider, state: str = None) -> str:
544
+ """Get OAuth2 authorization URL"""
545
+ if provider not in self.oauth_providers:
546
+ raise ValueError(f"OAuth provider {provider} not configured")
547
+
548
+ oauth_provider = self.oauth_providers[provider]
549
+ state = state or secrets.token_urlsafe(32)
550
+
551
+ params = {
552
+ "client_id": oauth_provider.client_id,
553
+ "redirect_uri": oauth_provider.redirect_uri,
554
+ "scope": " ".join(oauth_provider.scope),
555
+ "state": state,
556
+ "response_type": "code"
557
+ }
558
+
559
+ query_string = "&".join([f"{k}={v}" for k, v in params.items()])
560
+ return f"{oauth_provider.authorization_url}?{query_string}"
561
+
562
+ def get_user_permissions(self, user_id: str) -> List[str]:
563
+ """Get user permissions based on roles"""
564
+ if user_id not in self.users:
565
+ return []
566
+
567
+ user = self.users[user_id]
568
+ permissions = []
569
+
570
+ for role in user.roles:
571
+ if role == UserRole.ADMIN:
572
+ permissions.extend(["read", "write", "delete", "admin"])
573
+ elif role == UserRole.MODERATOR:
574
+ permissions.extend(["read", "write", "moderate"])
575
+ elif role == UserRole.USER:
576
+ permissions.extend(["read", "write"])
577
+ elif role == UserRole.API_USER:
578
+ permissions.extend(["api_access"])
579
+
580
+ return list(set(permissions))
581
+
582
+ def check_permission(self, user_id: str, permission: str) -> bool:
583
+ """Check if user has a specific permission"""
584
+ user_permissions = self.get_user_permissions(user_id)
585
+ return permission in user_permissions
586
+
587
+
588
+ # Global authentication system instance
589
+ auth_system = AdvancedAuthSystem()
590
+
591
+
592
+ # Convenience functions
593
+ def create_user(username: str, email: str, password: str, roles: List[UserRole] = None) -> Dict[str, Any]:
594
+ """Create a new user"""
595
+ return auth_system.register_user(username, email, password, roles)
596
+
597
+
598
+ def authenticate(email: str, password: str) -> Dict[str, Any]:
599
+ """Authenticate a user"""
600
+ return auth_system.authenticate_user(email, password)
601
+
602
+
603
+ def create_api_key(user_id: str, name: str, permissions: List[str] = None) -> Dict[str, Any]:
604
+ """Create an API key"""
605
+ return auth_system.create_api_key(user_id, name, permissions)
606
+
607
+
608
+ def setup_mfa(user_id: str) -> Dict[str, Any]:
609
+ """Setup MFA for a user"""
610
+ return auth_system.setup_mfa(user_id)