Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 

99 рядки
3.1 KiB

  1. # security.py
  2. from typing import Dict, Any, List, Optional
  3. import os
  4. import logging
  5. import httpx
  6. from jose import jwt, JWTError
  7. from fastapi import HTTPException, status, Depends
  8. from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
  9. logger = logging.getLogger("security")
  10. # === CONFIG ===
  11. KEYCLOAK_ISSUER = os.getenv(
  12. "KEYCLOAK_ISSUER",
  13. "https://192.168.1.3:10002/realms/API.Server.local",
  14. )
  15. KEYCLOAK_JWKS_URL = os.getenv(
  16. "KEYCLOAK_JWKS_URL",
  17. "https://192.168.1.3:10002/realms/API.Server.local/protocol/openid-connect/certs",
  18. )
  19. KEYCLOAK_AUDIENCE = os.getenv("KEYCLOAK_AUDIENCE", "Fastapi")
  20. ALGORITHMS = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"]
  21. # Per test con certificato self-signed. In prod: metti verify="/path/CA.crt"
  22. _http = httpx.AsyncClient(timeout=5.0, verify=False)
  23. _cached_jwks: Optional[Dict[str, Any]] = None
  24. # NON chiamarla 'security' per evitare conflitti col nome del modulo.
  25. http_bearer = HTTPBearer(auto_error=True)
  26. async def _get_jwks() -> Dict[str, Any]:
  27. global _cached_jwks
  28. if _cached_jwks is None:
  29. logger.info(f"Fetching JWKS from: {KEYCLOAK_JWKS_URL}")
  30. resp = await _http.get(KEYCLOAK_JWKS_URL)
  31. resp.raise_for_status()
  32. _cached_jwks = resp.json()
  33. return _cached_jwks
  34. async def _get_key(token: str) -> Dict[str, Any]:
  35. headers = jwt.get_unverified_header(token)
  36. kid = headers.get("kid")
  37. jwks = await _get_jwks()
  38. for key in jwks.get("keys", []):
  39. if key.get("kid") == kid:
  40. return key
  41. # chiave ruotata? invalida la cache e riprova
  42. global _cached_jwks
  43. _cached_jwks = None
  44. jwks = await _get_jwks()
  45. for key in jwks.get("keys", []):
  46. if key.get("kid") == kid:
  47. return key
  48. raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Signing key not found")
  49. async def verify_token(token: str) -> Dict[str, Any]:
  50. try:
  51. key = await _get_key(token)
  52. claims = jwt.decode(
  53. token,
  54. key,
  55. algorithms=ALGORITHMS,
  56. audience=KEYCLOAK_AUDIENCE,
  57. issuer=KEYCLOAK_ISSUER,
  58. options={"verify_aud": True, "verify_iss": True},
  59. )
  60. return claims
  61. except JWTError as e:
  62. raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
  63. async def get_current_user(
  64. credentials: HTTPAuthorizationCredentials = Depends(http_bearer),
  65. ) -> Dict[str, Any]:
  66. token = credentials.credentials
  67. return await verify_token(token)
  68. def require_roles(*roles: str):
  69. async def checker(claims: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
  70. # ruoli realm
  71. realm_roles: List[str] = (claims.get("realm_access") or {}).get("roles", []) or []
  72. # ruoli client
  73. client_roles: List[str] = []
  74. for v in (claims.get("resource_access") or {}).values():
  75. client_roles += v.get("roles", [])
  76. have = set(realm_roles + client_roles)
  77. missing = [r for r in roles if r not in have]
  78. if missing:
  79. raise HTTPException(status_code=403, detail=f"Missing roles: {missing}")
  80. return claims
  81. return checker