fix: manually handle session cookies for Authentik API authentication
Build and Push / build (release) Successful in 1m14s
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:
co-authored by
Claude Opus 4.5
parent
892015fa59
commit
faff45db90
+54
-33
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user