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