fix: manually handle session cookies for Authentik API authentication
Build and Push / build (release) Successful in 1m14s

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-01-01 21:01:29 +01:00
co-authored by Claude Opus 4.5
parent 892015fa59
commit faff45db90
3 changed files with 61 additions and 34 deletions
+54 -33
View File
@@ -249,24 +249,30 @@ class AuthService:
return items, total
def _get_csrf_token(self, client: httpx.AsyncClient) -> str:
"""Extract CSRF token from cookies"""
for cookie in client.cookies.jar:
if cookie.name == "authentik_csrf":
return cookie.value
def _extract_cookie(self, headers: httpx.Headers, cookie_name: str) -> str:
"""Extract a specific cookie value from Set-Cookie headers"""
import re
for header in headers.get_list('set-cookie'):
if header.startswith(f'{cookie_name}='):
match = re.match(rf'{cookie_name}=([^;]+)', header)
if match:
return match.group(1)
return ""
async def _authentik_session_login(self, client: httpx.AsyncClient) -> None:
async def _authentik_session_login(self, client: httpx.AsyncClient) -> str:
"""
Authenticate with Authentik using the flow API to establish a session
Authentik's flow API requires:
1. Cookie persistence between requests
1. Cookie persistence between requests (manually handled due to domain restrictions)
2. X-authentik-CSRF header set to the authentik_csrf cookie value
3. Multi-stage flow handling (identification -> password -> done)
Args:
client: httpx client with cookie persistence
client: httpx client
Returns:
Session cookie value for subsequent API calls
Raises:
ValueError: If authentication fails
@@ -278,55 +284,66 @@ class AuthService:
resp.raise_for_status()
data = resp.json()
logger.debug(f"Flow initial response: component={data.get('component')}, type={data.get('type')}")
# Extract cookies manually from Set-Cookie headers (bypasses domain restrictions)
session_cookie = self._extract_cookie(resp.headers, "authentik_session")
csrf_cookie = self._extract_cookie(resp.headers, "authentik_csrf")
# Get CSRF token for subsequent requests
csrf_token = self._get_csrf_token(client)
logger.debug(f"CSRF token obtained: {bool(csrf_token)}")
logger.debug(f"Flow initial: component={data.get('component')}, session={bool(session_cookie)}, csrf={bool(csrf_cookie)}")
# Build headers with CSRF token
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
if csrf_token:
headers["X-authentik-CSRF"] = csrf_token
# Build headers with manual cookie and CSRF token
def build_headers():
hdrs = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": f"authentik_session={session_cookie}",
}
if csrf_cookie:
hdrs["Cookie"] += f"; authentik_csrf={csrf_cookie}"
hdrs["X-authentik-CSRF"] = csrf_cookie
return hdrs
# Step 2: Handle identification stage - submit username
if data.get("component") == "ak-stage-identification":
resp = await client.post(
flow_url,
json={"uid_field": settings.authentik_username},
headers=headers,
headers=build_headers(),
)
resp.raise_for_status()
data = resp.json()
logger.debug(f"After username: component={data.get('component')}, type={data.get('type')}")
# Update CSRF token (might change between stages)
csrf_token = self._get_csrf_token(client)
if csrf_token:
headers["X-authentik-CSRF"] = csrf_token
# Update session cookie if new one received
new_session = self._extract_cookie(resp.headers, "authentik_session")
if new_session:
session_cookie = new_session
logger.debug(f"After username: component={data.get('component')}")
# Step 3: Handle password stage if required
if data.get("component") == "ak-stage-password":
resp = await client.post(
flow_url,
json={"password": settings.authentik_password},
headers=headers,
headers=build_headers(),
)
resp.raise_for_status()
data = resp.json()
logger.debug(f"After password: component={data.get('component')}, type={data.get('type')}")
# Update session cookie if new one received
new_session = self._extract_cookie(resp.headers, "authentik_session")
if new_session:
session_cookie = new_session
logger.debug(f"After password: component={data.get('component')}")
# Check for access denied
if data.get("component") == "ak-stage-access-denied":
raise ValueError("Authentik authentication failed: access denied")
# Check for redirect (successful auth)
if data.get("type") == "redirect" or data.get("to"):
if data.get("component") == "xak-flow-redirect" or data.get("to"):
logger.info("Successfully authenticated with Authentik via flow")
return
return session_cookie
# If we're still in identification stage, the username might be wrong
if data.get("component") == "ak-stage-identification":
@@ -334,6 +351,7 @@ class AuthService:
raise ValueError(f"Authentication stuck at identification stage: {response_errors}")
logger.info(f"Authentik flow completed with component: {data.get('component')}")
return session_cookie
async def bulk_sync_from_authentik(self) -> BulkSyncResultSchema:
"""
@@ -353,14 +371,17 @@ class AuthService:
try:
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
# Authenticate with Authentik to get session
await self._authentik_session_login(client)
# Authenticate with Authentik to get session cookie
session_cookie = await self._authentik_session_login(client)
# Fetch users from Authentik admin API using session
# Fetch users from Authentik admin API using session cookie
response = await client.get(
f"{settings.authentik_url}/api/v3/core/users/",
params={"page_size": 500},
headers={"Accept": "application/json"},
headers={
"Accept": "application/json",
"Cookie": f"authentik_session={session_cookie}",
},
)
if response.status_code == 401: