diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a82d1b..60de1e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.2] - 2026-01-04 + +### Fixed + +- `/auth/users/me` endpoint now supports both NPM forward auth headers AND JWT Bearer tokens + - Added `get_current_user_or_forward_auth()` combined auth dependency + - Fixes web authentication where NPM passes `X-authentik-*` headers instead of JWT + - Mobile/native clients continue to use JWT Bearer tokens as before + ## [1.9.1] - 2026-01-03 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 707d49f..b3eed28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "core-api" -version = "1.9.1" +version = "1.9.2" description = "Core Code API - Infrastructure management and tools API" readme = "README.md" requires-python = ">=3.12" diff --git a/src/domains/auth/controller.py b/src/domains/auth/controller.py index 99c5a61..2e15a82 100644 --- a/src/domains/auth/controller.py +++ b/src/domains/auth/controller.py @@ -20,7 +20,7 @@ from src.domains.auth.schemas import ( ApiKeysListResponse, ) from src.domains.auth.service import AuthService -from src.domains.auth.oidc import get_current_user +from src.domains.auth.oidc import get_current_user, get_current_user_or_forward_auth logger = get_logger(__name__) @@ -326,7 +326,7 @@ class AuthController(BaseController): }, ) async def get_current_user_profile( - user_claims: dict = Depends(get_current_user), + user_claims: dict = Depends(get_current_user_or_forward_auth), session: AsyncSession = Depends(get_async_session), ) -> UserProfileResponse: """ diff --git a/src/domains/auth/oidc.py b/src/domains/auth/oidc.py index 4824e0f..d24b917 100644 --- a/src/domains/auth/oidc.py +++ b/src/domains/auth/oidc.py @@ -396,6 +396,56 @@ async def get_forward_auth_admin( return user +async def get_current_user_or_forward_auth( + request: Request, + credentials: Optional[HTTPAuthorizationCredentials] = Security(security) +) -> Dict: + """ + Combined auth: Try forward auth headers first, then JWT Bearer token. + + Supports both: + - Web clients via NPM forward auth (X-authentik-* headers from proxy) + - Mobile/native clients via OIDC JWT Bearer tokens + + This is the preferred dependency for /auth/users/me and similar endpoints + that need to work with both web (cookie-based via NPM) and mobile (token-based). + + Args: + request: FastAPI request object containing headers + credentials: HTTP Bearer token from Authorization header + + Returns: + User claims dictionary with at minimum: sub, email, name, groups, auth_method + + Raises: + HTTPException 401: If neither forward auth headers nor valid JWT provided + """ + # 1. Try forward auth headers first (web via NPM) + username = request.headers.get("x-authentik-username") + email = request.headers.get("x-authentik-email") + + if username and email: + # Forward auth headers present - use them + groups = request.headers.get("x-authentik-groups", "") + name = request.headers.get("x-authentik-name", username) + uid = request.headers.get("x-authentik-uid") + + user_info = { + "sub": uid, # Use authentik UID as subject (for user lookup) + "email": email, + "preferred_username": username, + "name": name, + "groups": [g.strip() for g in groups.split(",")] if groups else [], + "auth_method": "forward_auth" + } + + logger.info(f"Authenticated via forward auth: {email}") + return user_info + + # 2. Fall back to JWT Bearer token (mobile/native) + return await get_current_user(credentials) + + # ============================================================================= # Permission-Based Access Control # =============================================================================