feat: implement local authentication mode and update related components

This commit is contained in:
Abhimanyu Saharan
2026-02-11 19:10:23 +05:30
parent 0ff645f795
commit 06ff1a9720
23 changed files with 563 additions and 93 deletions

View File

@@ -1,8 +1,9 @@
"""User authentication helpers backed by Clerk JWT verification."""
"""User authentication helpers for Clerk and local-token auth modes."""
from __future__ import annotations
from dataclasses import dataclass
from hmac import compare_digest
from typing import TYPE_CHECKING, Literal
import httpx
@@ -29,6 +30,9 @@ logger = get_logger(__name__)
security = HTTPBearer(auto_error=False)
SECURITY_DEP = Depends(security)
SESSION_DEP = Depends(get_session)
LOCAL_AUTH_USER_ID = "local-auth-user"
LOCAL_AUTH_EMAIL = "admin@home.local"
LOCAL_AUTH_NAME = "Local User"
class ClerkTokenPayload(BaseModel):
@@ -45,6 +49,18 @@ class AuthContext:
user: User | None = None
def _extract_bearer_token(authorization: str | None) -> str | None:
if not authorization:
return None
value = authorization.strip()
if not value:
return None
if not value.lower().startswith("bearer "):
return None
token = value.split(" ", maxsplit=1)[1].strip()
return token or None
def _non_empty_str(value: object) -> str | None:
if not isinstance(value, str):
return None
@@ -228,6 +244,9 @@ async def _fetch_clerk_profile(clerk_user_id: str) -> tuple[str | None, str | No
async def delete_clerk_user(clerk_user_id: str) -> None:
"""Delete a Clerk user via the official Clerk SDK."""
if settings.auth_mode != "clerk":
return
secret = settings.clerk_secret_key.strip()
secret_kind = secret.split("_", maxsplit=1)[0] if "_" in secret else "unknown"
server_url = _normalize_clerk_server_url(settings.clerk_api_url or "")
@@ -343,6 +362,55 @@ async def _get_or_sync_user(
return user
async def _get_or_create_local_user(session: AsyncSession) -> User:
defaults: dict[str, object] = {
"email": LOCAL_AUTH_EMAIL,
"name": LOCAL_AUTH_NAME,
}
user, _created = await crud.get_or_create(
session,
User,
clerk_user_id=LOCAL_AUTH_USER_ID,
defaults=defaults,
)
changed = False
if not user.email:
user.email = LOCAL_AUTH_EMAIL
changed = True
if not user.name:
user.name = LOCAL_AUTH_NAME
changed = True
if changed:
session.add(user)
await session.commit()
await session.refresh(user)
from app.services.organizations import ensure_member_for_user
await ensure_member_for_user(session, user)
return user
async def _resolve_local_auth_context(
*,
request: Request,
session: AsyncSession,
required: bool,
) -> AuthContext | None:
token = _extract_bearer_token(request.headers.get("Authorization"))
if token is None:
if required:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return None
expected = settings.local_auth_token.strip()
if not expected or not compare_digest(token, expected):
if required:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return None
user = await _get_or_create_local_user(session)
return AuthContext(actor_type="user", user=user)
def _parse_subject(claims: dict[str, object]) -> str | None:
payload = ClerkTokenPayload.model_validate(claims)
return payload.sub
@@ -353,7 +421,17 @@ async def get_auth_context(
credentials: HTTPAuthorizationCredentials | None = SECURITY_DEP,
session: AsyncSession = SESSION_DEP,
) -> AuthContext:
"""Resolve required authenticated user context from Clerk JWT headers."""
"""Resolve required authenticated user context for the configured auth mode."""
if settings.auth_mode == "local":
local_auth = await _resolve_local_auth_context(
request=request,
session=session,
required=True,
)
if local_auth is None: # pragma: no cover
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return local_auth
request_state = await _authenticate_clerk_request(request)
if request_state.status != AuthStatus.SIGNED_IN or not isinstance(request_state.payload, dict):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
@@ -388,6 +466,13 @@ async def get_auth_context_optional(
"""Resolve user context if available, otherwise return `None`."""
if request.headers.get("X-Agent-Token"):
return None
if settings.auth_mode == "local":
return await _resolve_local_auth_context(
request=request,
session=session,
required=False,
)
request_state = await _authenticate_clerk_request(request)
if request_state.status != AuthStatus.SIGNED_IN or not isinstance(request_state.payload, dict):
return None