feat(auth): implement GET /auth/me for NPM forward auth
Add endpoint to get current user profile from NPM forward auth headers. Enables web authentication flow where NPM handles Authentik login. - Read X-authentik-* headers set by NPM forward auth - Auto-create user if not in database (first login via web) - Sync roles from current Authentik groups - Add get_user_by_email and get_user_by_authentik_id helpers 🤖 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
8bd13eab09
commit
884996fd83
+70
-7
@@ -13,6 +13,7 @@ from src.logging_config import get_logger
|
|||||||
from src.db import get_async_session
|
from src.db import get_async_session
|
||||||
from src.auth.schemas import AuthSyncRequest, AuthSyncResponse, UsersListResponse, BulkSyncResultSchema, GroupsListResponse
|
from src.auth.schemas import AuthSyncRequest, AuthSyncResponse, UsersListResponse, BulkSyncResultSchema, GroupsListResponse
|
||||||
from src.auth.service import AuthService
|
from src.auth.service import AuthService
|
||||||
|
from src.auth.oidc import get_forward_auth_user
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -222,21 +223,83 @@ class AuthController(BaseController):
|
|||||||
responses={
|
responses={
|
||||||
200: {"description": "User profile"},
|
200: {"description": "User profile"},
|
||||||
401: {"description": "Not authenticated"},
|
401: {"description": "Not authenticated"},
|
||||||
|
404: {"description": "User not found in database"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def get_me(
|
async def get_me(
|
||||||
|
forward_auth_user: Optional[dict] = Depends(get_forward_auth_user),
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> JSONResponse:
|
) -> AuthSyncResponse:
|
||||||
"""
|
"""
|
||||||
Get the current authenticated user's profile
|
Get the current authenticated user's profile
|
||||||
|
|
||||||
Note: This endpoint requires a valid session or API key.
|
Authentication is handled by NPM forward auth with Authentik.
|
||||||
For now, returns 501 Not Implemented until session management is added.
|
The proxy sets X-authentik-* headers which this endpoint reads.
|
||||||
|
|
||||||
|
For internal/LAN access (no forward auth headers), returns 401.
|
||||||
|
Use POST /auth/sync with an OIDC token for mobile app authentication.
|
||||||
"""
|
"""
|
||||||
# TODO: Implement with get_current_user dependency
|
# Require forward auth for this endpoint
|
||||||
raise HTTPException(
|
if forward_auth_user is None:
|
||||||
status_code=501,
|
raise HTTPException(
|
||||||
detail="Not implemented - use /auth/sync with access token",
|
status_code=401,
|
||||||
|
detail="Authentication required - access via authenticated proxy or use /auth/sync",
|
||||||
|
)
|
||||||
|
|
||||||
|
service = AuthService(session)
|
||||||
|
|
||||||
|
# Try to find user by Authentik UID first, then by email
|
||||||
|
user = None
|
||||||
|
uid = forward_auth_user.get("uid")
|
||||||
|
if uid:
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
authentik_id = uuid.UUID(uid)
|
||||||
|
user = await service.get_user_by_authentik_id(authentik_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass # Invalid UUID, try email
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
email = forward_auth_user.get("email")
|
||||||
|
if email:
|
||||||
|
user = await service.get_user_by_email(email)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
# User authenticated with Authentik but not synced to database yet
|
||||||
|
# This can happen on first login via web
|
||||||
|
logger.info(f"User {forward_auth_user.get('email')} not found, creating from forward auth")
|
||||||
|
|
||||||
|
# Create user from forward auth headers
|
||||||
|
from src.auth.schemas import TokenInfoSchema
|
||||||
|
token_info = TokenInfoSchema(
|
||||||
|
sub=forward_auth_user.get("uid", ""),
|
||||||
|
email=forward_auth_user.get("email", ""),
|
||||||
|
name=forward_auth_user.get("name"),
|
||||||
|
groups=forward_auth_user.get("groups", []),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user, _ = await service.sync_user(token_info)
|
||||||
|
await service.sync_roles(user, token_info.groups)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(user, ["preferences", "roles"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create user from forward auth: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Failed to create user profile",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sync roles from current groups (in case they changed)
|
||||||
|
groups = forward_auth_user.get("groups", [])
|
||||||
|
roles = await service.sync_roles(user, groups)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return AuthSyncResponse(
|
||||||
|
user=service.user_to_schema(user),
|
||||||
|
roles=service.roles_to_schema(roles),
|
||||||
|
preferences=service.preferences_to_schema(user.preferences),
|
||||||
|
is_new_user=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
@@ -86,6 +86,42 @@ class AuthService:
|
|||||||
logger.error(f"Authentik userinfo request error: {e}")
|
logger.error(f"Authentik userinfo request error: {e}")
|
||||||
raise ValueError("Authentication service unavailable")
|
raise ValueError("Authentication service unavailable")
|
||||||
|
|
||||||
|
async def get_user_by_email(self, email: str) -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Get user by email address
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: User email address
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User if found, None otherwise
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(User)
|
||||||
|
.options(selectinload(User.roles), selectinload(User.preferences))
|
||||||
|
.where(User.email == email)
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_user_by_authentik_id(self, authentik_id: uuid.UUID) -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Get user by Authentik UUID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
authentik_id: Authentik user UUID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User if found, None otherwise
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(User)
|
||||||
|
.options(selectinload(User.roles), selectinload(User.preferences))
|
||||||
|
.where(User.authentik_id == authentik_id)
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
async def sync_user(self, token_info: TokenInfoSchema) -> tuple[User, bool]:
|
async def sync_user(self, token_info: TokenInfoSchema) -> tuple[User, bool]:
|
||||||
"""
|
"""
|
||||||
Create or update user from OIDC token info
|
Create or update user from OIDC token info
|
||||||
|
|||||||
Reference in New Issue
Block a user