Add Authentik OIDC login
Same Authlib-based pattern as v1: SessionMiddleware + core/auth.py (login/callback/logout + require_login dependency), gating "/", /asr/* and /tts/* behind Authentik (application slug "asr-v2", provider pk 14). The websocket route can't use a FastAPI dependency (no Request in its scope) so it's split into its own router and checks websocket.session manually before accept(). v2 has no public domain yet, so the redirect_uri points at the internal 172.30.1.41:18101 address over plain http — hence https_only=False on the session cookie. Client id/secret and the session secret live in /srv/asr/env/asr.env under ASR_V2_-prefixed names (that env file is shared with v1, which already owns the unprefixed OIDC_* names). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
66
app/core/auth.py
Normal file
66
app/core/auth.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from core.config import env_str
|
||||
|
||||
OIDC_ISSUER = env_str("ASR_V2_OIDC_ISSUER", "")
|
||||
OIDC_CLIENT_ID = env_str("ASR_V2_OIDC_CLIENT_ID", "")
|
||||
OIDC_CLIENT_SECRET = env_str("ASR_V2_OIDC_CLIENT_SECRET", "")
|
||||
OIDC_REDIRECT_URI = env_str("ASR_V2_OIDC_REDIRECT_URI", "")
|
||||
SESSION_SECRET_KEY = env_str("ASR_V2_SESSION_SECRET_KEY", "")
|
||||
|
||||
oauth = OAuth()
|
||||
oauth.register(
|
||||
name="authentik",
|
||||
client_id=OIDC_CLIENT_ID,
|
||||
client_secret=OIDC_CLIENT_SECRET,
|
||||
server_metadata_url=f"{OIDC_ISSUER}.well-known/openid-configuration",
|
||||
client_kwargs={"scope": "openid email profile"},
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
async def login(request: Request, next: str = "/"):
|
||||
request.session["next"] = next if next.startswith("/") else "/"
|
||||
return await oauth.authentik.authorize_redirect(request, OIDC_REDIRECT_URI)
|
||||
|
||||
|
||||
@router.get("/callback")
|
||||
async def callback(request: Request):
|
||||
try:
|
||||
token = await oauth.authentik.authorize_access_token(request)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=401, detail=f"로그인 실패: {exc}") from exc
|
||||
|
||||
userinfo = token.get("userinfo") or {}
|
||||
request.session["user"] = {
|
||||
"sub": userinfo.get("sub"),
|
||||
"username": userinfo.get("preferred_username") or userinfo.get("nickname"),
|
||||
"email": userinfo.get("email"),
|
||||
"name": userinfo.get("name"),
|
||||
}
|
||||
next_path = request.session.pop("next", "/") or "/"
|
||||
return RedirectResponse(url=next_path)
|
||||
|
||||
|
||||
@router.get("/logout")
|
||||
async def logout(request: Request):
|
||||
request.session.pop("user", None)
|
||||
return RedirectResponse(url="/")
|
||||
|
||||
|
||||
def require_login(request: Request) -> Dict[str, Any]:
|
||||
user = request.session.get("user")
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=303,
|
||||
headers={"Location": f"/auth/login?next={request.url.path}"},
|
||||
)
|
||||
return user
|
||||
Reference in New Issue
Block a user