Initial implementation of OAuth models, endpoints, and migrations

- Added models for `OAuthClient`, `OAuthState`, and `OAuthAccount`.
- Created Pydantic schemas to support OAuth flows, client management, and linked accounts.
- Implemented skeleton endpoints for OAuth Provider mode: authorization, token, and revocation.
- Updated router imports to include new `/oauth` and `/oauth/provider` routes.
- Added Alembic migration script to create OAuth-related database tables.
- Enhanced `users` table to allow OAuth-only accounts by making `password_hash` nullable.
This commit is contained in:
Felipe Cardoso
2025-11-25 00:37:23 +01:00
parent e6792c2d6c
commit 16ee4e0cb3
23 changed files with 4109 additions and 13 deletions

View File

@@ -0,0 +1,45 @@
"""OAuth state model for CSRF protection during OAuth flows."""
from sqlalchemy import Column, DateTime, String
from sqlalchemy.dialects.postgresql import UUID
from .base import Base, TimestampMixin, UUIDMixin
class OAuthState(Base, UUIDMixin, TimestampMixin):
"""
Temporary storage for OAuth state parameters.
Prevents CSRF attacks during OAuth flows by storing a random state
value that must match on callback. Also stores PKCE code_verifier
for the Authorization Code flow with PKCE.
These records are short-lived (10 minutes by default) and should
be deleted after use or expiration.
"""
__tablename__ = "oauth_states"
# Random state parameter (CSRF protection)
state = Column(String(255), unique=True, nullable=False, index=True)
# PKCE code_verifier (used to generate code_challenge)
code_verifier = Column(String(128), nullable=True)
# OIDC nonce for ID token replay protection
nonce = Column(String(255), nullable=True)
# OAuth provider (google, github, etc.)
provider = Column(String(50), nullable=False)
# Original redirect URI (for callback validation)
redirect_uri = Column(String(500), nullable=True)
# User ID if this is an account linking flow (user is already logged in)
user_id = Column(UUID(as_uuid=True), nullable=True)
# Expiration time
expires_at = Column(DateTime(timezone=True), nullable=False)
def __repr__(self):
return f"<OAuthState {self.state[:8]}... ({self.provider})>"