feat(auth): harden Authentik and guest capability boundaries

This commit is contained in:
Jens
2026-08-30 05:59:49 +02:00
parent b93d926b94
commit 96f90373dc
15 changed files with 928 additions and 8 deletions
+91 -1
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
import logging
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, Request, Response, status
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.core.config import get_settings
@@ -10,14 +12,22 @@ from app.core.errors import AppError
from app.db.session import get_db
from app.schemas.auth import AuthLoginRequest, AuthSession, AuthSessionEnvelope
from app.services.auth_service import AuthPrincipal, AuthService
from app.services.authentik_oidc_service import AuthentikOidcService
from app.services.demo_workflow_service import DemoWorkflowService
router = APIRouter(prefix="/auth", tags=["auth"])
COOKIE_NAME = "geointel_session"
OIDC_FLOW_COOKIE_NAME = "geointel_oidc_flow"
logger = logging.getLogger("geointel.auth")
def _session_from_principal(principal: AuthPrincipal, *, guest_access_enabled: bool) -> AuthSession:
def _session_from_principal(
principal: AuthPrincipal,
*,
guest_access_enabled: bool,
authentik_enabled: bool,
) -> AuthSession:
return AuthSession(
authentication_required=True,
authenticated=True,
@@ -25,6 +35,7 @@ def _session_from_principal(principal: AuthPrincipal, *, guest_access_enabled: b
expires_at=datetime.fromtimestamp(principal.expires_at, tz=UTC),
role=principal.role,
guest_access_enabled=guest_access_enabled,
authentik_enabled=authentik_enabled,
guest_project_id=principal.project_id,
)
@@ -32,11 +43,13 @@ def _session_from_principal(principal: AuthPrincipal, *, guest_access_enabled: b
def _session_payload(request: Request) -> AuthSession:
settings = get_settings()
guest_access_enabled = settings.auth_enabled and settings.guest_access_enabled
authentik_enabled = AuthentikOidcService(settings).enabled
if not settings.auth_enabled:
return AuthSession(
authentication_required=False,
authenticated=True,
guest_access_enabled=False,
authentik_enabled=False,
)
principal = AuthService.verify_session_token(request.cookies.get(COOKIE_NAME), settings)
if principal is None:
@@ -44,10 +57,12 @@ def _session_payload(request: Request) -> AuthSession:
authentication_required=True,
authenticated=False,
guest_access_enabled=guest_access_enabled,
authentik_enabled=authentik_enabled,
)
return _session_from_principal(
principal,
guest_access_enabled=guest_access_enabled,
authentik_enabled=authentik_enabled,
)
@@ -120,10 +135,83 @@ def login(payload: AuthLoginRequest, request: Request, response: Response) -> Au
data=_session_from_principal(
principal,
guest_access_enabled=settings.guest_access_enabled,
authentik_enabled=AuthentikOidcService(settings).enabled,
)
)
@router.get("/authentik/start")
def authentik_start(request: Request) -> RedirectResponse:
settings = get_settings()
service = AuthentikOidcService(settings)
try:
location, flow = service.start()
except Exception as exc:
logger.warning("Authentik authorization start failed: %s", type(exc).__name__)
raise AppError(
code="AUTHENTIK_UNAVAILABLE",
message="Authentik is momenteel niet beschikbaar.",
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
) from exc
response = RedirectResponse(location, status_code=status.HTTP_302_FOUND)
response.set_cookie(
OIDC_FLOW_COOKIE_NAME,
flow,
max_age=600,
httponly=True,
secure=True,
samesite="lax",
path=f"{settings.api_prefix}/auth/authentik",
)
return response
@router.get("/authentik/callback")
def authentik_callback(
request: Request,
code: str = "",
state: str = "",
) -> RedirectResponse:
settings = get_settings()
service = AuthentikOidcService(settings)
base_url = settings.public_base_url.rstrip("/")
try:
service.finish(
code=code,
state=state,
flow_cookie=request.cookies.get(OIDC_FLOW_COOKIE_NAME, ""),
)
token = AuthService.create_session_token(
settings.auth_username or "operator",
settings,
)
except Exception as exc:
logger.warning("Authentik callback rejected: %s", type(exc).__name__)
response = RedirectResponse(
f"{base_url}/?authentik=error",
status_code=status.HTTP_302_FOUND,
)
else:
response = RedirectResponse(
f"{base_url}/",
status_code=status.HTTP_302_FOUND,
)
_set_session_cookie(
request=request,
response=response,
token=token,
max_age=settings.auth_session_ttl_seconds,
)
response.delete_cookie(
OIDC_FLOW_COOKIE_NAME,
path=f"{settings.api_prefix}/auth/authentik",
secure=True,
httponly=True,
samesite="lax",
)
return response
@router.post("/guest", response_model=AuthSessionEnvelope)
def guest_login(
request: Request,
@@ -163,6 +251,7 @@ def guest_login(
data=_session_from_principal(
principal,
guest_access_enabled=True,
authentik_enabled=AuthentikOidcService(settings).enabled,
)
)
@@ -176,5 +265,6 @@ def logout(response: Response) -> AuthSessionEnvelope:
authentication_required=settings.auth_enabled,
authenticated=not settings.auth_enabled,
guest_access_enabled=settings.auth_enabled and settings.guest_access_enabled,
authentik_enabled=AuthentikOidcService(settings).enabled,
)
)