fix: switch frontend login to authentik auth-code flow

This commit is contained in:
Chris
2026-03-30 01:04:28 +08:00
parent 5e46c58dd4
commit a170f0a681
2 changed files with 87 additions and 1 deletions

View File

@@ -1,12 +1,20 @@
import logging
import secrets
from urllib.parse import urljoin from urllib.parse import urljoin
import httpx import httpx
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from app.core.config import get_settings from app.core.config import get_settings
from app.schemas.login import LoginRequest, LoginResponse from app.schemas.login import (
LoginRequest,
LoginResponse,
OIDCAuthUrlResponse,
OIDCCodeExchangeRequest,
)
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
logger = logging.getLogger(__name__)
def _resolve_username_by_email(settings, email: str) -> str | None: def _resolve_username_by_email(settings, email: str) -> str | None:
@@ -87,6 +95,7 @@ def login(payload: LoginRequest) -> LoginResponse:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="authentik_unreachable") from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="authentik_unreachable") from exc
if resp.status_code >= 400: if resp.status_code >= 400:
logger.warning("authentik password grant failed: status=%s body=%s", resp.status_code, resp.text)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_username_or_password") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_username_or_password")
data = resp.json() data = resp.json()
@@ -100,3 +109,71 @@ def login(payload: LoginRequest) -> LoginResponse:
expires_in=data.get("expires_in"), expires_in=data.get("expires_in"),
scope=data.get("scope"), scope=data.get("scope"),
) )
@router.get("/oidc/url", response_model=OIDCAuthUrlResponse)
def get_oidc_authorize_url(redirect_uri: str) -> OIDCAuthUrlResponse:
settings = get_settings()
client_id = settings.authentik_client_id or settings.authentik_audience
if not settings.authentik_base_url or not client_id:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="authentik_login_not_configured")
authorize_endpoint = urljoin(settings.authentik_base_url.rstrip("/") + "/", "application/o/authorize/")
state = secrets.token_urlsafe(24)
params = httpx.QueryParams(
{
"client_id": client_id,
"response_type": "code",
"scope": "openid profile email",
"redirect_uri": redirect_uri,
"state": state,
"prompt": "login",
}
)
return OIDCAuthUrlResponse(authorize_url=f"{authorize_endpoint}?{params}")
@router.post("/oidc/exchange", response_model=LoginResponse)
def exchange_oidc_code(payload: OIDCCodeExchangeRequest) -> LoginResponse:
settings = get_settings()
client_id = settings.authentik_client_id or settings.authentik_audience
if not settings.authentik_base_url or not client_id or not settings.authentik_client_secret:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="authentik_login_not_configured")
token_endpoint = settings.authentik_token_endpoint or urljoin(
settings.authentik_base_url.rstrip("/") + "/", "application/o/token/"
)
form = {
"grant_type": "authorization_code",
"client_id": client_id,
"client_secret": settings.authentik_client_secret,
"code": payload.code,
"redirect_uri": payload.redirect_uri,
}
try:
resp = httpx.post(
token_endpoint,
data=form,
timeout=10,
verify=settings.authentik_verify_tls,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="authentik_unreachable") from exc
if resp.status_code >= 400:
logger.warning("authentik auth-code exchange failed: status=%s body=%s", resp.status_code, resp.text)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="authentik_code_exchange_failed")
data = resp.json()
token = data.get("access_token")
if not token:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="authentik_missing_access_token")
return LoginResponse(
access_token=token,
token_type=data.get("token_type", "Bearer"),
expires_in=data.get("expires_in"),
scope=data.get("scope"),
)

View File

@@ -11,3 +11,12 @@ class LoginResponse(BaseModel):
token_type: str = "Bearer" token_type: str = "Bearer"
expires_in: int | None = None expires_in: int | None = None
scope: str | None = None scope: str | None = None
class OIDCAuthUrlResponse(BaseModel):
authorize_url: str
class OIDCCodeExchangeRequest(BaseModel):
code: str
redirect_uri: str