hydra_service.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import logging
  2. from typing import List
  3. import ory_hydra_client
  4. from ory_hydra_client.api import o_auth2_api
  5. from ory_hydra_client.models.accept_o_auth2_login_request import AcceptOAuth2LoginRequest
  6. from ory_hydra_client.models.reject_o_auth2_request import RejectOAuth2Request
  7. from ory_hydra_client.models.accept_o_auth2_consent_request import AcceptOAuth2ConsentRequest
  8. from ory_hydra_client.models.o_auth2_consent_session import OAuth2ConsentSession
  9. import requests
  10. from app.core.hydra_config import hydra_settings
  11. logger = logging.getLogger(__name__)
  12. class HydraService:
  13. def __init__(self):
  14. configuration = ory_hydra_client.Configuration(
  15. host=hydra_settings.HYDRA_ADMIN_URL
  16. )
  17. self.api_client = ory_hydra_client.ApiClient(configuration)
  18. self.oauth2 = o_auth2_api.OAuth2Api(self.api_client)
  19. # Admin REST base,用于调用 /clients 等管理接口
  20. self.admin_base = hydra_settings.HYDRA_ADMIN_URL.rstrip("/")
  21. # ---------- OAuth2 Client 管理 ----------
  22. def create_or_update_client(
  23. self,
  24. client_id: str,
  25. client_secret: str,
  26. redirect_uris: List[str],
  27. client_name: str = "",
  28. ) -> None:
  29. """
  30. 在 Hydra 中创建或更新一个 OAuth2 Client,使其 client_id 与应用 app_id 对齐。
  31. - 如果已存在同名 Client,则先尝试删除再重建(简单幂等实现)。
  32. - grant_types / scope 等使用平台推荐的默认配置。
  33. """
  34. if not redirect_uris:
  35. logger.warning(
  36. "尝试在 Hydra 创建 Client 时未提供 redirect_uris,client_id=%s",
  37. client_id,
  38. )
  39. payload = {
  40. "client_id": client_id,
  41. "client_secret": client_secret,
  42. "client_name": client_name or client_id,
  43. "redirect_uris": redirect_uris,
  44. "grant_types": ["authorization_code", "refresh_token"],
  45. "response_types": ["code"],
  46. "scope": "openid offline profile",
  47. "token_endpoint_auth_method": "client_secret_basic",
  48. }
  49. # 1. 尝试删除已有的同名 Client(容错即可)
  50. try:
  51. resp_del = requests.delete(
  52. f"{self.admin_base}/admin/clients/{client_id}", timeout=5
  53. )
  54. if resp_del.status_code not in (200, 204, 404):
  55. logger.warning(
  56. "删除 Hydra Client 可能失败(忽略继续创建): status=%s, body=%s",
  57. resp_del.status_code,
  58. resp_del.text,
  59. )
  60. except Exception as e:
  61. logger.warning("删除 Hydra Client 异常(忽略): %s", e)
  62. # 2. 创建新的 Client
  63. resp = requests.post(
  64. f"{self.admin_base}/admin/clients", json=payload, timeout=5
  65. )
  66. if resp.status_code not in (200, 201):
  67. logger.error(
  68. "在 Hydra 创建 Client 失败: status=%s, body=%s, url=%s, payload=%s",
  69. resp.status_code,
  70. resp.text,
  71. f"{self.admin_base}/admin/clients",
  72. payload,
  73. )
  74. raise Exception(f"创建 Hydra OIDC Client 失败: {resp.text}")
  75. def get_login_request(self, challenge: str):
  76. try:
  77. return self.oauth2.get_o_auth2_login_request(challenge)
  78. except Exception as e:
  79. logger.error(f"获取登录请求失败 (challenge: {challenge}): {e}")
  80. raise
  81. def accept_login_request(self, challenge: str, subject: str):
  82. body = AcceptOAuth2LoginRequest(
  83. subject=subject,
  84. remember=True,
  85. remember_for=3600,
  86. )
  87. try:
  88. logger.info(f"接受登录请求 (subject: {subject}, challenge: {challenge})")
  89. return self.oauth2.accept_o_auth2_login_request(challenge, accept_o_auth2_login_request=body)
  90. except Exception as e:
  91. logger.error(f"接受登录请求失败 (challenge: {challenge}): {e}")
  92. raise
  93. def reject_login_request(self, challenge: str, error: str, error_description: str):
  94. body = RejectOAuth2Request(
  95. error=error,
  96. error_description=error_description
  97. )
  98. try:
  99. logger.info(f"拒绝登录请求 (challenge: {challenge}, error: {error})")
  100. return self.oauth2.reject_o_auth2_login_request(challenge, reject_o_auth2_request=body)
  101. except Exception as e:
  102. logger.error(f"拒绝登录请求失败 (challenge: {challenge}): {e}")
  103. raise
  104. def get_consent_request(self, challenge: str):
  105. try:
  106. return self.oauth2.get_o_auth2_consent_request(challenge)
  107. except Exception as e:
  108. logger.error(f"获取同意请求失败 (challenge: {challenge}): {e}")
  109. raise
  110. def accept_consent_request(self, challenge: str, grant_scope: list, id_token_claims: dict):
  111. body = AcceptOAuth2ConsentRequest(
  112. grant_scope=grant_scope,
  113. grant_access_token_audience=[],
  114. remember=True,
  115. remember_for=3600,
  116. session=OAuth2ConsentSession(
  117. id_token=id_token_claims
  118. )
  119. )
  120. try:
  121. logger.info(f"接受同意请求 (challenge: {challenge}, scope: {grant_scope})")
  122. return self.oauth2.accept_o_auth2_consent_request(challenge, accept_o_auth2_consent_request=body)
  123. except Exception as e:
  124. logger.error(f"接受同意请求失败 (challenge: {challenge}): {e}")
  125. raise
  126. def reject_consent_request(self, challenge: str, error: str, error_description: str):
  127. body = RejectOAuth2Request(
  128. error=error,
  129. error_description=error_description
  130. )
  131. try:
  132. logger.info(f"拒绝同意请求 (challenge: {challenge}, error: {error})")
  133. return self.oauth2.reject_o_auth2_consent_request(challenge, reject_o_auth2_request=body)
  134. except Exception as e:
  135. logger.error(f"拒绝同意请求失败 (challenge: {challenge}): {e}")
  136. raise
  137. hydra_service = HydraService()