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