feat: Add Auth Service with JWT authentication

This commit is contained in:
Apple
2025-11-26 11:47:00 -08:00
parent 2c4eb7d432
commit 5aaf6cbf21
23 changed files with 3522 additions and 26 deletions

View File

@@ -0,0 +1,145 @@
"""
Shared Auth Middleware for DAARION Services
Use this in agents-service, microdao-service, city-service, secondme-service
"""
from fastapi import Request, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional, List
from jose import jwt, JWTError
import os
# JWT Configuration - must match auth-service
JWT_SECRET = os.getenv("AUTH_JWT_SECRET", "your-very-long-secret-key-change-in-production")
JWT_ALGORITHM = "HS256"
# Security scheme
security = HTTPBearer(auto_error=False)
class AuthUser:
"""Authenticated user context"""
def __init__(
self,
user_id: str,
email: str,
display_name: Optional[str],
roles: List[str]
):
self.user_id = user_id
self.email = email
self.display_name = display_name
self.roles = roles
def has_role(self, role: str) -> bool:
return role in self.roles
def is_admin(self) -> bool:
return "admin" in self.roles
def __repr__(self):
return f"AuthUser(user_id={self.user_id}, email={self.email}, roles={self.roles})"
def decode_token(token: str) -> Optional[dict]:
"""Decode and validate a JWT token"""
try:
payload = jwt.decode(
token,
JWT_SECRET,
algorithms=[JWT_ALGORITHM],
options={"verify_exp": True}
)
# Verify it's an access token
if payload.get("type") != "access":
return None
return payload
except JWTError:
return None
async def get_current_user_optional(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> Optional[AuthUser]:
"""
Get current user if authenticated, None otherwise.
Use this for endpoints that work both with and without auth.
"""
if not credentials:
return None
payload = decode_token(credentials.credentials)
if not payload:
return None
return AuthUser(
user_id=payload.get("sub"),
email=payload.get("email"),
display_name=payload.get("name"),
roles=payload.get("roles", [])
)
async def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> AuthUser:
"""
Get current user, raise 401 if not authenticated.
Use this for protected endpoints.
"""
if not credentials:
raise HTTPException(
status_code=401,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}
)
payload = decode_token(credentials.credentials)
if not payload:
raise HTTPException(
status_code=401,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"}
)
return AuthUser(
user_id=payload.get("sub"),
email=payload.get("email"),
display_name=payload.get("name"),
roles=payload.get("roles", [])
)
def require_role(role: str):
"""
Dependency that requires a specific role.
Usage: @app.get("/admin", dependencies=[Depends(require_role("admin"))])
"""
async def role_checker(user: AuthUser = Depends(get_current_user)):
if not user.has_role(role):
raise HTTPException(
status_code=403,
detail=f"Role '{role}' required"
)
return user
return role_checker
def require_any_role(roles: List[str]):
"""
Dependency that requires any of the specified roles.
"""
async def role_checker(user: AuthUser = Depends(get_current_user)):
if not any(user.has_role(r) for r in roles):
raise HTTPException(
status_code=403,
detail=f"One of roles {roles} required"
)
return user
return role_checker
# Convenience aliases
RequireAuth = Depends(get_current_user)
OptionalAuth = Depends(get_current_user_optional)
RequireAdmin = Depends(require_role("admin"))