""" Servicio de autenticación con Keycloak """ from keycloak import KeycloakOpenID, KeycloakAdmin from keycloak.exceptions import KeycloakError from fastapi import HTTPException from sqlalchemy.orm import Session import logging from core.config import settings from .dto import ( LoginRequestDTO, TokenResponseDTO, RefreshTokenRequestDTO, UserInfoResponseDTO, LogoutRequestDTO ) logger = logging.getLogger(__name__) class AuthService: """Servicio de autenticación""" def __init__(self, db: Session): self.db = db self.keycloak_openid = KeycloakOpenID( server_url=settings.KEYCLOAK_SERVER_URL, client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=settings.KEYCLOAK_REALM, client_secret_key=settings.KEYCLOAK_CLIENT_SECRET ) def login(self, login_data: LoginRequestDTO) -> TokenResponseDTO: """ Autentica usuario y obtiene tokens Args: login_data: Credenciales de login Returns: TokenResponseDTO con access_token y refresh_token Raises: HTTPException: Si las credenciales son inválidas """ try: # Verificar que el tenant existe from api.v1.modules.a76.tenants.service import TenantService tenant_service = TenantService(self.db) tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug) if not tenant: raise HTTPException(status_code=404, detail="Tenant not found") if not tenant.is_active: raise HTTPException(status_code=403, detail="Tenant is not active") # Cambiar realm al del tenant self.keycloak_openid.realm_name = tenant.keycloak_realm # Obtener token de Keycloak token_response = self.keycloak_openid.token( username=login_data.username, password=login_data.password ) logger.info(f"User logged in: {login_data.username} (tenant: {tenant.slug})") return TokenResponseDTO( access_token=token_response["access_token"], refresh_token=token_response["refresh_token"], token_type="bearer", expires_in=token_response["expires_in"] ) except KeycloakError as e: logger.warning(f"Keycloak authentication failed: {str(e)}") raise HTTPException(status_code=401, detail="Invalid credentials") except HTTPException: raise except Exception as e: logger.error(f"Login error: {str(e)}") raise HTTPException(status_code=500, detail="Authentication error") def refresh_token(self, refresh_data: RefreshTokenRequestDTO) -> TokenResponseDTO: """ Refresca el access token usando refresh token Args: refresh_data: Refresh token Returns: TokenResponseDTO con nuevos tokens """ try: token_response = self.keycloak_openid.refresh_token( refresh_data.refresh_token ) return TokenResponseDTO( access_token=token_response["access_token"], refresh_token=token_response["refresh_token"], token_type="bearer", expires_in=token_response["expires_in"] ) except KeycloakError as e: logger.warning(f"Token refresh failed: {str(e)}") raise HTTPException(status_code=401, detail="Invalid or expired refresh token") except Exception as e: logger.error(f"Token refresh error: {str(e)}") raise HTTPException(status_code=500, detail="Token refresh error") def get_user_info(self, access_token: str) -> UserInfoResponseDTO: """ Obtiene información del usuario desde el token Args: access_token: Access token JWT Returns: UserInfoResponseDTO con información del usuario """ try: user_info = self.keycloak_openid.userinfo(access_token) # Extraer roles roles = [] if "realm_access" in user_info: roles = user_info["realm_access"].get("roles", []) # Extraer tenant_id si está presente tenant_id = user_info.get("tenant_id") if not tenant_id and "attributes" in user_info: tenant_id = user_info["attributes"].get("tenant_id") return UserInfoResponseDTO( sub=user_info.get("sub"), email=user_info.get("email"), name=user_info.get("name"), preferred_username=user_info.get("preferred_username"), tenant_id=int(tenant_id) if tenant_id else None, roles=roles ) except KeycloakError as e: logger.warning(f"Get user info failed: {str(e)}") raise HTTPException(status_code=401, detail="Invalid token") except Exception as e: logger.error(f"Get user info error: {str(e)}") raise HTTPException(status_code=500, detail="Error retrieving user info") def logout(self, logout_data: LogoutRequestDTO) -> dict: """ Cierra sesión invalidando el refresh token Args: logout_data: Refresh token a invalidar Returns: Dict con mensaje de éxito """ try: self.keycloak_openid.logout(logout_data.refresh_token) logger.info("User logged out successfully") return {"message": "Logged out successfully"} except KeycloakError as e: logger.warning(f"Logout failed: {str(e)}") # No lanzamos error aquí, el logout puede fallar si el token ya expiró return {"message": "Logged out"} except Exception as e: logger.error(f"Logout error: {str(e)}") raise HTTPException(status_code=500, detail="Logout error")