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.auth.schemas import AuthSyncRequest, AuthSyncResponse, UsersListResponse, BulkSyncResultSchema, GroupsListResponse
|
||||
from src.auth.service import AuthService
|
||||
from src.auth.oidc import get_forward_auth_user
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -222,21 +223,83 @@ class AuthController(BaseController):
|
||||
responses={
|
||||
200: {"description": "User profile"},
|
||||
401: {"description": "Not authenticated"},
|
||||
404: {"description": "User not found in database"},
|
||||
},
|
||||
)
|
||||
async def get_me(
|
||||
forward_auth_user: Optional[dict] = Depends(get_forward_auth_user),
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
) -> JSONResponse:
|
||||
) -> AuthSyncResponse:
|
||||
"""
|
||||
Get the current authenticated user's profile
|
||||
|
||||
Note: This endpoint requires a valid session or API key.
|
||||
For now, returns 501 Not Implemented until session management is added.
|
||||
Authentication is handled by NPM forward auth with Authentik.
|
||||
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
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Not implemented - use /auth/sync with access token",
|
||||
# Require forward auth for this endpoint
|
||||
if forward_auth_user is None:
|
||||
raise HTTPException(
|
||||
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
|
||||
|
||||
@@ -86,6 +86,42 @@ class AuthService:
|
||||
logger.error(f"Authentik userinfo request error: {e}")
|
||||
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]:
|
||||
"""
|
||||
Create or update user from OIDC token info
|
||||
|
||||
Reference in New Issue
Block a user