feat(FN-004): merge fusion/fn-004

This commit is contained in:
Fusion
2026-05-14 06:47:30 +02:00
parent 4cbd30ff42
commit 3a18a1f170
62 changed files with 2567 additions and 11 deletions
+3
View File
@@ -0,0 +1,3 @@
from app.auth.dependencies import get_current_active_user, get_current_user
__all__ = ["get_current_user", "get_current_active_user"]
+93
View File
@@ -0,0 +1,93 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.jwt import decode_token
from app.config import settings
from app.db import get_db_session
from app.models.user import User
bearer_scheme = HTTPBearer(auto_error=False)
async def _get_or_create_dev_user(session: AsyncSession) -> User:
"""Return or create the fixed development user."""
result = await session.execute(
select(User).where(User.authentik_sub == "dev-user")
)
user = result.scalar_one_or_none()
if user is None:
user = User(
authentik_sub="dev-user",
email="dev@localhost",
display_name="Dev User",
is_active=True,
)
session.add(user)
await session.commit()
await session.refresh(user)
return user
async def get_current_user(
token: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
session: AsyncSession = Depends(get_db_session),
) -> User:
if token is None:
if settings.debug and settings.auth_dev_bypass:
return await _get_or_create_dev_user(session)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
try:
claims = decode_token(token.credentials)
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid token: {exc}",
headers={"WWW-Authenticate": "Bearer"},
) from exc
authentik_sub = claims.get("sub")
email = claims.get("email", "")
display_name = claims.get("name") or claims.get("preferred_username") or email
if not authentik_sub:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token missing 'sub' claim",
headers={"WWW-Authenticate": "Bearer"},
)
result = await session.execute(
select(User).where(User.authentik_sub == authentik_sub)
)
user = result.scalar_one_or_none()
if user is None:
user = User(
authentik_sub=authentik_sub,
email=email,
display_name=display_name,
is_active=True,
)
session.add(user)
await session.commit()
await session.refresh(user)
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
)
return current_user
+53
View File
@@ -0,0 +1,53 @@
from typing import Any
import jwt
from app.config import settings
def decode_token(token: str) -> dict[str, Any]:
"""Decode a JWT token.
When authentik_issuer_url is configured, validates the token
against the OIDC discovery document JWKS.
Otherwise, decodes without verification (local development only).
"""
if settings.authentik_issuer_url:
import httpx
issuer = settings.authentik_issuer_url.rstrip("/")
discovery_url = f"{issuer}/.well-known/openid-configuration"
with httpx.Client() as client:
resp = client.get(discovery_url)
resp.raise_for_status()
discovery = resp.json()
jwks_uri = discovery["jwks_uri"]
jwks_resp = client.get(jwks_uri)
jwks_resp.raise_for_status()
jwks = jwks_resp.json()
signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(
_find_matching_key(jwks, token)
)
return jwt.decode(
token,
signing_key, # type: ignore[arg-type]
algorithms=["RS256"],
audience=settings.authentik_client_id,
issuer=settings.authentik_issuer_url,
)
return jwt.decode(token, options={"verify_signature": False})
def _find_matching_key(jwks: dict[str, Any], token: str) -> dict[str, Any]:
"""Find the key in JWKS that matches the token's kid header."""
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
for key in jwks.get("keys", []):
key_dict: dict[str, Any] = key
if key_dict.get("kid") == kid:
return key_dict
raise RuntimeError(f"No matching JWKS key found for kid={kid}")