auth-kit-fastapi 0.1.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.
- auth_kit_fastapi/__init__.py +27 -0
- auth_kit_fastapi/schemas/__init__.py +84 -0
- auth_kit_fastapi/schemas/auth.py +145 -0
- auth_kit_fastapi/schemas/passkey.py +129 -0
- auth_kit_fastapi/schemas/two_factor.py +73 -0
- auth_kit_fastapi/services/__init__.py +17 -0
- auth_kit_fastapi/services/email_service.py +468 -0
- auth_kit_fastapi/services/passkey_service.py +461 -0
- auth_kit_fastapi/services/two_factor_service.py +422 -0
- auth_kit_fastapi/services/user_service.py +407 -0
- auth_kit_fastapi-0.1.0.dist-info/METADATA +226 -0
- auth_kit_fastapi-0.1.0.dist-info/RECORD +20 -0
- auth_kit_fastapi-0.1.0.dist-info/WHEEL +5 -0
- auth_kit_fastapi-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/conftest.py +198 -0
- tests/test_auth_endpoints.py +350 -0
- tests/test_passkey_endpoints.py +282 -0
- tests/test_services.py +449 -0
- tests/test_two_factor_endpoints.py +377 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth Kit FastAPI - Complete authentication solution for FastAPI applications
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core.config import AuthConfig
|
|
6
|
+
from .core.app import create_auth_app
|
|
7
|
+
from .core.dependencies import (
|
|
8
|
+
get_current_user,
|
|
9
|
+
get_current_active_user,
|
|
10
|
+
require_verified_user,
|
|
11
|
+
require_superuser
|
|
12
|
+
)
|
|
13
|
+
from .models.user import BaseUser
|
|
14
|
+
from .core.events import auth_events
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AuthConfig",
|
|
20
|
+
"create_auth_app",
|
|
21
|
+
"get_current_user",
|
|
22
|
+
"get_current_active_user",
|
|
23
|
+
"require_verified_user",
|
|
24
|
+
"require_superuser",
|
|
25
|
+
"BaseUser",
|
|
26
|
+
"auth_events"
|
|
27
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export all schemas
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .auth import (
|
|
6
|
+
UserBase,
|
|
7
|
+
UserCreate,
|
|
8
|
+
UserUpdate,
|
|
9
|
+
UserResponse,
|
|
10
|
+
LoginRequest,
|
|
11
|
+
TokenResponse,
|
|
12
|
+
LoginResponse,
|
|
13
|
+
RefreshTokenRequest,
|
|
14
|
+
LogoutRequest,
|
|
15
|
+
PasswordChangeRequest,
|
|
16
|
+
PasswordResetRequest,
|
|
17
|
+
PasswordResetConfirmRequest,
|
|
18
|
+
EmailVerificationRequest,
|
|
19
|
+
MessageResponse,
|
|
20
|
+
ErrorResponse
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .passkey import (
|
|
24
|
+
PasskeyBase,
|
|
25
|
+
PasskeyCreate,
|
|
26
|
+
PasskeyResponse,
|
|
27
|
+
PasskeyListResponse,
|
|
28
|
+
RegistrationOptionsResponse,
|
|
29
|
+
RegistrationCompleteRequest,
|
|
30
|
+
AuthenticationOptionsResponse,
|
|
31
|
+
AuthenticationCompleteRequest,
|
|
32
|
+
AuthenticationBeginRequest
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .two_factor import (
|
|
36
|
+
TwoFactorSetupResponse,
|
|
37
|
+
TwoFactorVerifyRequest,
|
|
38
|
+
TwoFactorEnableResponse,
|
|
39
|
+
TwoFactorDisableRequest,
|
|
40
|
+
RecoveryCodesRegenerateRequest,
|
|
41
|
+
RecoveryCodesResponse,
|
|
42
|
+
TwoFactorLoginVerifyRequest,
|
|
43
|
+
TwoFactorStatusResponse
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Auth schemas
|
|
48
|
+
"UserBase",
|
|
49
|
+
"UserCreate",
|
|
50
|
+
"UserUpdate",
|
|
51
|
+
"UserResponse",
|
|
52
|
+
"LoginRequest",
|
|
53
|
+
"TokenResponse",
|
|
54
|
+
"LoginResponse",
|
|
55
|
+
"RefreshTokenRequest",
|
|
56
|
+
"LogoutRequest",
|
|
57
|
+
"PasswordChangeRequest",
|
|
58
|
+
"PasswordResetRequest",
|
|
59
|
+
"PasswordResetConfirmRequest",
|
|
60
|
+
"EmailVerificationRequest",
|
|
61
|
+
"MessageResponse",
|
|
62
|
+
"ErrorResponse",
|
|
63
|
+
|
|
64
|
+
# Passkey schemas
|
|
65
|
+
"PasskeyBase",
|
|
66
|
+
"PasskeyCreate",
|
|
67
|
+
"PasskeyResponse",
|
|
68
|
+
"PasskeyListResponse",
|
|
69
|
+
"RegistrationOptionsResponse",
|
|
70
|
+
"RegistrationCompleteRequest",
|
|
71
|
+
"AuthenticationOptionsResponse",
|
|
72
|
+
"AuthenticationCompleteRequest",
|
|
73
|
+
"AuthenticationBeginRequest",
|
|
74
|
+
|
|
75
|
+
# 2FA schemas
|
|
76
|
+
"TwoFactorSetupResponse",
|
|
77
|
+
"TwoFactorVerifyRequest",
|
|
78
|
+
"TwoFactorEnableResponse",
|
|
79
|
+
"TwoFactorDisableRequest",
|
|
80
|
+
"RecoveryCodesRegenerateRequest",
|
|
81
|
+
"RecoveryCodesResponse",
|
|
82
|
+
"TwoFactorLoginVerifyRequest",
|
|
83
|
+
"TwoFactorStatusResponse"
|
|
84
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication schemas for request/response validation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
from pydantic import BaseModel, EmailStr, Field, validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Base schemas
|
|
12
|
+
class UserBase(BaseModel):
|
|
13
|
+
"""Base user schema"""
|
|
14
|
+
email: EmailStr
|
|
15
|
+
first_name: Optional[str] = None
|
|
16
|
+
last_name: Optional[str] = None
|
|
17
|
+
phone_number: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UserCreate(UserBase):
|
|
21
|
+
"""User registration schema"""
|
|
22
|
+
password: str = Field(..., min_length=8, max_length=128)
|
|
23
|
+
confirm_password: Optional[str] = None
|
|
24
|
+
accept_terms: Optional[bool] = False
|
|
25
|
+
|
|
26
|
+
@validator('confirm_password')
|
|
27
|
+
def passwords_match(cls, v, values):
|
|
28
|
+
if 'password' in values and v != values['password']:
|
|
29
|
+
raise ValueError('Passwords do not match')
|
|
30
|
+
return v
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UserUpdate(BaseModel):
|
|
34
|
+
"""User profile update schema"""
|
|
35
|
+
first_name: Optional[str] = None
|
|
36
|
+
last_name: Optional[str] = None
|
|
37
|
+
phone_number: Optional[str] = None
|
|
38
|
+
avatar_url: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UserResponse(UserBase):
|
|
42
|
+
"""User response schema"""
|
|
43
|
+
id: UUID
|
|
44
|
+
is_active: bool
|
|
45
|
+
is_verified: bool
|
|
46
|
+
is_superuser: bool
|
|
47
|
+
two_factor_enabled: bool
|
|
48
|
+
created_at: datetime
|
|
49
|
+
updated_at: datetime
|
|
50
|
+
last_login_at: Optional[datetime] = None
|
|
51
|
+
|
|
52
|
+
class Config:
|
|
53
|
+
from_attributes = True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Authentication schemas
|
|
57
|
+
class LoginRequest(BaseModel):
|
|
58
|
+
"""Login request schema (OAuth2 compatible)"""
|
|
59
|
+
username: EmailStr # OAuth2 spec requires 'username'
|
|
60
|
+
password: str
|
|
61
|
+
grant_type: Optional[str] = "password"
|
|
62
|
+
scope: Optional[str] = ""
|
|
63
|
+
client_id: Optional[str] = None
|
|
64
|
+
client_secret: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TokenResponse(BaseModel):
|
|
68
|
+
"""Token response schema (OAuth2 compatible)"""
|
|
69
|
+
access_token: str
|
|
70
|
+
refresh_token: Optional[str] = None
|
|
71
|
+
token_type: str = "bearer"
|
|
72
|
+
expires_in: Optional[int] = None
|
|
73
|
+
scope: Optional[str] = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LoginResponse(BaseModel):
|
|
77
|
+
"""Enhanced login response with user data"""
|
|
78
|
+
user: UserResponse
|
|
79
|
+
tokens: TokenResponse
|
|
80
|
+
requires_2fa: bool = False
|
|
81
|
+
message: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RefreshTokenRequest(BaseModel):
|
|
85
|
+
"""Refresh token request"""
|
|
86
|
+
refresh_token: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LogoutRequest(BaseModel):
|
|
90
|
+
"""Logout request"""
|
|
91
|
+
refresh_token: str
|
|
92
|
+
everywhere: bool = False # Logout from all devices
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Password management schemas
|
|
96
|
+
class PasswordChangeRequest(BaseModel):
|
|
97
|
+
"""Password change request"""
|
|
98
|
+
current_password: str
|
|
99
|
+
new_password: str = Field(..., min_length=8, max_length=128)
|
|
100
|
+
confirm_password: Optional[str] = None
|
|
101
|
+
|
|
102
|
+
@validator('confirm_password')
|
|
103
|
+
def passwords_match(cls, v, values):
|
|
104
|
+
if 'new_password' in values and v != values['new_password']:
|
|
105
|
+
raise ValueError('Passwords do not match')
|
|
106
|
+
return v
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PasswordResetRequest(BaseModel):
|
|
110
|
+
"""Password reset request"""
|
|
111
|
+
email: EmailStr
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PasswordResetConfirmRequest(BaseModel):
|
|
115
|
+
"""Password reset confirmation"""
|
|
116
|
+
token: str
|
|
117
|
+
new_password: str = Field(..., min_length=8, max_length=128)
|
|
118
|
+
confirm_password: Optional[str] = None
|
|
119
|
+
|
|
120
|
+
@validator('confirm_password')
|
|
121
|
+
def passwords_match(cls, v, values):
|
|
122
|
+
if 'new_password' in values and v != values['new_password']:
|
|
123
|
+
raise ValueError('Passwords do not match')
|
|
124
|
+
return v
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Email verification schemas
|
|
128
|
+
class EmailVerificationRequest(BaseModel):
|
|
129
|
+
"""Request email verification resend"""
|
|
130
|
+
email: Optional[EmailStr] = None # Use current user's email if not provided
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Response messages
|
|
134
|
+
class MessageResponse(BaseModel):
|
|
135
|
+
"""Generic message response"""
|
|
136
|
+
message: str
|
|
137
|
+
success: bool = True
|
|
138
|
+
data: Optional[Dict[str, Any]] = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ErrorResponse(BaseModel):
|
|
142
|
+
"""Error response"""
|
|
143
|
+
detail: str
|
|
144
|
+
code: Optional[str] = None
|
|
145
|
+
field: Optional[str] = None
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Passkey/WebAuthn schemas
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional, List, Dict, Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PasskeyBase(BaseModel):
|
|
12
|
+
"""Base passkey schema"""
|
|
13
|
+
name: str = Field(..., min_length=3, max_length=255)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PasskeyCreate(PasskeyBase):
|
|
17
|
+
"""Passkey registration request"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PasskeyResponse(PasskeyBase):
|
|
22
|
+
"""Passkey response"""
|
|
23
|
+
id: UUID
|
|
24
|
+
credential_id: str
|
|
25
|
+
authenticator_type: str
|
|
26
|
+
is_discoverable: bool
|
|
27
|
+
created_at: datetime
|
|
28
|
+
last_used_at: Optional[datetime] = None
|
|
29
|
+
|
|
30
|
+
class Config:
|
|
31
|
+
from_attributes = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PasskeyListResponse(BaseModel):
|
|
35
|
+
"""List of user's passkeys"""
|
|
36
|
+
passkeys: List[PasskeyResponse]
|
|
37
|
+
total: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# WebAuthn schemas
|
|
41
|
+
class PublicKeyCredentialRpEntity(BaseModel):
|
|
42
|
+
"""Relying Party entity"""
|
|
43
|
+
id: str
|
|
44
|
+
name: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PublicKeyCredentialUserEntity(BaseModel):
|
|
48
|
+
"""User entity"""
|
|
49
|
+
id: str
|
|
50
|
+
name: str
|
|
51
|
+
displayName: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PublicKeyCredentialParameters(BaseModel):
|
|
55
|
+
"""Credential parameters"""
|
|
56
|
+
type: str = "public-key"
|
|
57
|
+
alg: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PublicKeyCredentialDescriptor(BaseModel):
|
|
61
|
+
"""Credential descriptor"""
|
|
62
|
+
type: str = "public-key"
|
|
63
|
+
id: str
|
|
64
|
+
transports: Optional[List[str]] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AuthenticatorSelectionCriteria(BaseModel):
|
|
68
|
+
"""Authenticator selection criteria"""
|
|
69
|
+
authenticatorAttachment: Optional[str] = None
|
|
70
|
+
residentKey: Optional[str] = None
|
|
71
|
+
userVerification: Optional[str] = "preferred"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RegistrationOptionsResponse(BaseModel):
|
|
75
|
+
"""WebAuthn registration options"""
|
|
76
|
+
challenge: str
|
|
77
|
+
rp: PublicKeyCredentialRpEntity
|
|
78
|
+
user: PublicKeyCredentialUserEntity
|
|
79
|
+
pubKeyCredParams: List[PublicKeyCredentialParameters]
|
|
80
|
+
timeout: Optional[int] = 60000
|
|
81
|
+
excludeCredentials: Optional[List[PublicKeyCredentialDescriptor]] = []
|
|
82
|
+
authenticatorSelection: Optional[AuthenticatorSelectionCriteria] = None
|
|
83
|
+
attestation: Optional[str] = "none"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RegistrationCredentialResponse(BaseModel):
|
|
87
|
+
"""Registration credential from client"""
|
|
88
|
+
id: str
|
|
89
|
+
rawId: str
|
|
90
|
+
type: str = "public-key"
|
|
91
|
+
response: Dict[str, Any]
|
|
92
|
+
clientExtensionResults: Optional[Dict[str, Any]] = None
|
|
93
|
+
authenticatorAttachment: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RegistrationCompleteRequest(BaseModel):
|
|
97
|
+
"""Complete registration request"""
|
|
98
|
+
name: str
|
|
99
|
+
response: RegistrationCredentialResponse
|
|
100
|
+
challenge: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AuthenticationOptionsResponse(BaseModel):
|
|
104
|
+
"""WebAuthn authentication options"""
|
|
105
|
+
challenge: str
|
|
106
|
+
timeout: Optional[int] = 60000
|
|
107
|
+
rpId: Optional[str] = None
|
|
108
|
+
allowCredentials: Optional[List[PublicKeyCredentialDescriptor]] = []
|
|
109
|
+
userVerification: Optional[str] = "preferred"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AuthenticationCredentialResponse(BaseModel):
|
|
113
|
+
"""Authentication credential from client"""
|
|
114
|
+
id: str
|
|
115
|
+
rawId: str
|
|
116
|
+
type: str = "public-key"
|
|
117
|
+
response: Dict[str, Any]
|
|
118
|
+
clientExtensionResults: Optional[Dict[str, Any]] = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AuthenticationCompleteRequest(BaseModel):
|
|
122
|
+
"""Complete authentication request"""
|
|
123
|
+
response: AuthenticationCredentialResponse
|
|
124
|
+
challenge: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AuthenticationBeginRequest(BaseModel):
|
|
128
|
+
"""Begin authentication request"""
|
|
129
|
+
email: Optional[str] = None # For discoverable credentials
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Two-Factor Authentication schemas
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from pydantic import BaseModel, Field, validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TwoFactorSetupResponse(BaseModel):
|
|
10
|
+
"""2FA setup response"""
|
|
11
|
+
secret: str
|
|
12
|
+
qr_code: str # Base64 encoded QR code image
|
|
13
|
+
manual_entry_key: str
|
|
14
|
+
message: str = "Scan the QR code with your authenticator app"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TwoFactorVerifyRequest(BaseModel):
|
|
18
|
+
"""Verify 2FA code"""
|
|
19
|
+
code: str = Field(..., regex="^[0-9]{6}$")
|
|
20
|
+
|
|
21
|
+
@validator('code')
|
|
22
|
+
def validate_code(cls, v):
|
|
23
|
+
if not v.isdigit() or len(v) != 6:
|
|
24
|
+
raise ValueError('Code must be 6 digits')
|
|
25
|
+
return v
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TwoFactorEnableResponse(BaseModel):
|
|
29
|
+
"""2FA enable response"""
|
|
30
|
+
recovery_codes: List[str]
|
|
31
|
+
message: str = "Two-factor authentication has been enabled"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TwoFactorDisableRequest(BaseModel):
|
|
35
|
+
"""Disable 2FA request"""
|
|
36
|
+
password: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RecoveryCodesRegenerateRequest(BaseModel):
|
|
40
|
+
"""Regenerate recovery codes request"""
|
|
41
|
+
password: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RecoveryCodesResponse(BaseModel):
|
|
45
|
+
"""Recovery codes response"""
|
|
46
|
+
recovery_codes: List[str]
|
|
47
|
+
message: str = "New recovery codes have been generated"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TwoFactorLoginVerifyRequest(BaseModel):
|
|
51
|
+
"""Verify 2FA during login"""
|
|
52
|
+
code: str
|
|
53
|
+
is_recovery_code: bool = False
|
|
54
|
+
|
|
55
|
+
@validator('code')
|
|
56
|
+
def validate_code(cls, v, values):
|
|
57
|
+
if values.get('is_recovery_code'):
|
|
58
|
+
# Recovery code format: XXXX-XXXX-XXXX-XXXX
|
|
59
|
+
if not v.replace('-', '').isalnum():
|
|
60
|
+
raise ValueError('Invalid recovery code format')
|
|
61
|
+
else:
|
|
62
|
+
# TOTP code: 6 digits
|
|
63
|
+
if not v.isdigit() or len(v) != 6:
|
|
64
|
+
raise ValueError('Code must be 6 digits')
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TwoFactorStatusResponse(BaseModel):
|
|
69
|
+
"""2FA status response"""
|
|
70
|
+
enabled: bool
|
|
71
|
+
method: Optional[str] = None
|
|
72
|
+
backup_codes_remaining: Optional[int] = None
|
|
73
|
+
created_at: Optional[str] = None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service classes for auth-kit-fastapi
|
|
3
|
+
|
|
4
|
+
Provides business logic layer for authentication operations
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .user_service import UserService
|
|
8
|
+
from .email_service import EmailService
|
|
9
|
+
from .passkey_service import PasskeyService
|
|
10
|
+
from .two_factor_service import TwoFactorService
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"UserService",
|
|
14
|
+
"EmailService",
|
|
15
|
+
"PasskeyService",
|
|
16
|
+
"TwoFactorService"
|
|
17
|
+
]
|