160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
"""
|
|
Authentication Schemas
|
|
|
|
Pydantic schemas for authentication and OIDC-related requests/responses.
|
|
"""
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
from typing import Optional, List
|
|
|
|
|
|
# ==========================================
|
|
# OIDC Authorization Schemas
|
|
# ==========================================
|
|
|
|
class AuthorizationRequest(BaseModel):
|
|
"""Schema for OIDC authorization request."""
|
|
client_id: str = Field(..., description="OAuth client ID")
|
|
redirect_uri: str = Field(..., description="Callback URL")
|
|
response_type: str = Field("code", description="Response type (only 'code' supported)")
|
|
scope: str = Field("openid", description="Requested scopes (space-separated)")
|
|
state: Optional[str] = Field(None, description="CSRF protection state")
|
|
|
|
@field_validator('response_type')
|
|
@classmethod
|
|
def validate_response_type(cls, v: str) -> str:
|
|
"""Validate that response_type is 'code'."""
|
|
if v != "code":
|
|
raise ValueError("Only 'code' response_type is supported (Authorization Code Flow)")
|
|
return v
|
|
|
|
@field_validator('scope')
|
|
@classmethod
|
|
def validate_scope(cls, v: str) -> str:
|
|
"""Validate that scope includes 'openid'."""
|
|
scopes = v.split()
|
|
if 'openid' not in scopes:
|
|
raise ValueError("Scope must include 'openid'")
|
|
return v
|
|
|
|
|
|
class AuthorizationResponse(BaseModel):
|
|
"""Schema for OIDC authorization response."""
|
|
code: str = Field(..., description="Authorization code")
|
|
state: Optional[str] = Field(None, description="State from request")
|
|
|
|
|
|
# ==========================================
|
|
# OIDC Token Schemas
|
|
# ==========================================
|
|
|
|
class TokenRequest(BaseModel):
|
|
"""Schema for OIDC token request."""
|
|
grant_type: str = Field(..., description="Grant type (authorization_code)")
|
|
code: str = Field(..., description="Authorization code")
|
|
redirect_uri: str = Field(..., description="Redirect URI (must match authorization request)")
|
|
client_id: str = Field(..., description="OAuth client ID")
|
|
client_secret: str = Field(..., description="OAuth client secret")
|
|
|
|
@field_validator('grant_type')
|
|
@classmethod
|
|
def validate_grant_type(cls, v: str) -> str:
|
|
"""Validate grant_type."""
|
|
if v != "authorization_code":
|
|
raise ValueError("Only 'authorization_code' grant_type is supported")
|
|
return v
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
"""Schema for OIDC token response."""
|
|
access_token: str = Field(..., description="Access token")
|
|
token_type: str = Field("Bearer", description="Token type")
|
|
expires_in: int = Field(..., description="Token expiration time in seconds")
|
|
id_token: str = Field(..., description="OpenID Connect ID token")
|
|
scope: str = Field(..., description="Granted scopes")
|
|
|
|
|
|
# ==========================================
|
|
# UserInfo Schemas
|
|
# ==========================================
|
|
|
|
class UserInfoResponse(BaseModel):
|
|
"""Schema for OIDC UserInfo response."""
|
|
sub: str = Field(..., description="Subject identifier (user ID)")
|
|
username: str
|
|
email: str
|
|
name: str
|
|
preferred_username: str
|
|
role: str
|
|
permissions: List[str]
|
|
|
|
|
|
# ==========================================
|
|
# Login/Registration Schemas
|
|
# ==========================================
|
|
|
|
class LoginRequest(BaseModel):
|
|
"""Schema for user login."""
|
|
username: str = Field(..., description="Username")
|
|
password: str = Field(..., description="Password")
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
"""Schema for login response."""
|
|
success: bool
|
|
message: str
|
|
user: Optional[dict] = None
|
|
redirect_url: Optional[str] = None
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
"""Schema for user registration."""
|
|
username: str = Field(..., min_length=3, max_length=80)
|
|
email: str = Field(..., description="Email address")
|
|
name: str = Field(..., min_length=1, max_length=120)
|
|
password: str = Field(..., min_length=8)
|
|
password_confirm: str = Field(..., description="Password confirmation")
|
|
preferred_username: Optional[str] = Field(None, max_length=80)
|
|
|
|
@field_validator('password')
|
|
@classmethod
|
|
def validate_password(cls, v: str) -> str:
|
|
"""Validate password strength."""
|
|
if len(v) < 8:
|
|
raise ValueError('Password must be at least 8 characters long')
|
|
return v
|
|
|
|
@field_validator('password_confirm')
|
|
@classmethod
|
|
def passwords_match(cls, v: str, info) -> str:
|
|
"""Validate that passwords match."""
|
|
if 'password' in info.data and v != info.data['password']:
|
|
raise ValueError('Passwords do not match')
|
|
return v
|
|
|
|
|
|
class RegisterResponse(BaseModel):
|
|
"""Schema for registration response."""
|
|
success: bool
|
|
message: str
|
|
user_id: Optional[int] = None
|
|
|
|
|
|
# ==========================================
|
|
# OIDC Discovery Schemas
|
|
# ==========================================
|
|
|
|
class OIDCDiscoveryResponse(BaseModel):
|
|
"""Schema for OIDC discovery document."""
|
|
issuer: str
|
|
authorization_endpoint: str
|
|
token_endpoint: str
|
|
userinfo_endpoint: str
|
|
jwks_uri: str
|
|
response_types_supported: List[str]
|
|
subject_types_supported: List[str]
|
|
id_token_signing_alg_values_supported: List[str]
|
|
scopes_supported: List[str]
|
|
token_endpoint_auth_methods_supported: List[str]
|
|
claims_supported: List[str]
|