oauth.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import calendar
  2. import json
  3. import logging
  4. import time
  5. from urllib.parse import urlparse, urlencode
  6. from typing import Callable, Optional
  7. from alibabacloud_credentials.provider.refreshable import Credentials, RefreshResult, RefreshCachedSupplier
  8. from alibabacloud_credentials.http import HttpOptions
  9. from Tea.core import TeaCore
  10. from alibabacloud_credentials_api import ICredentialsProvider
  11. from alibabacloud_credentials.utils import parameter_helper as ph
  12. from alibabacloud_credentials.exceptions import CredentialException
  13. log = logging.getLogger('credentials')
  14. log.setLevel(logging.INFO)
  15. ch = logging.StreamHandler()
  16. log.addHandler(ch)
  17. # OAuth 令牌更新回调函数类型
  18. OAuthTokenUpdateCallback = Callable[[str, str, str, str, str, int, int], None]
  19. OAuthTokenUpdateCallbackAsync = Callable[[str, str, str, str, str, int, int], None]
  20. def _get_stale_time(expiration: int) -> int:
  21. if expiration < 0:
  22. return int(time.mktime(time.localtime())) + 60 * 60
  23. return expiration - 15 * 60
  24. class OAuthCredentialsProvider(ICredentialsProvider):
  25. DEFAULT_CONNECT_TIMEOUT = 5000
  26. DEFAULT_READ_TIMEOUT = 10000
  27. def __init__(self, *,
  28. client_id: str = None,
  29. sign_in_url: str = None,
  30. access_token: str = None,
  31. access_token_expire: int = 0,
  32. refresh_token: str = None,
  33. http_options: HttpOptions = None,
  34. token_update_callback: Optional[OAuthTokenUpdateCallback] = None,
  35. token_update_callback_async: Optional[OAuthTokenUpdateCallbackAsync] = None):
  36. if not client_id:
  37. raise ValueError('the ClientId is empty')
  38. if not sign_in_url:
  39. raise ValueError('the url for sign-in is empty')
  40. self._client_id = client_id
  41. self._sign_in_url = sign_in_url
  42. self._access_token = access_token
  43. self._access_token_expire = access_token_expire
  44. self._refresh_token = refresh_token
  45. self._token_update_callback = token_update_callback
  46. self._token_update_callback_async = token_update_callback_async
  47. self._http_options = http_options if http_options is not None else HttpOptions()
  48. self._runtime_options = {
  49. 'connectTimeout': self._http_options.connect_timeout if self._http_options.connect_timeout is not None else OAuthCredentialsProvider.DEFAULT_CONNECT_TIMEOUT,
  50. 'readTimeout': self._http_options.read_timeout if self._http_options.read_timeout is not None else OAuthCredentialsProvider.DEFAULT_READ_TIMEOUT,
  51. 'httpsProxy': self._http_options.proxy
  52. }
  53. self._credentials_cache = RefreshCachedSupplier(
  54. refresh_callable=self._refresh_credentials,
  55. refresh_callable_async=self._refresh_credentials_async,
  56. )
  57. def get_credentials(self) -> Credentials:
  58. return self._credentials_cache._sync_call()
  59. async def get_credentials_async(self) -> Credentials:
  60. return await self._credentials_cache._async_call()
  61. def _try_refresh_oauth_token(self) -> None:
  62. current_time = int(time.mktime(time.localtime()))
  63. # 构建刷新令牌请求
  64. r = urlparse(self._sign_in_url)
  65. tea_request = ph.get_new_request()
  66. tea_request.headers['host'] = r.hostname
  67. tea_request.port = r.port
  68. tea_request.protocol = r.scheme
  69. tea_request.method = 'POST'
  70. tea_request.pathname = '/v1/token'
  71. # 设置请求体
  72. body_data = {
  73. 'grant_type': 'refresh_token',
  74. 'refresh_token': self._refresh_token,
  75. 'client_id': self._client_id,
  76. 'Timestamp': ph.get_iso_8061_date()
  77. }
  78. tea_request.body = urlencode(body_data)
  79. tea_request.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  80. response = TeaCore.do_action(tea_request, self._runtime_options)
  81. if response.status_code != 200:
  82. raise CredentialException(f"failed to refresh OAuth token, status code: {response.status_code}, response: {response.body.decode('utf-8')}")
  83. # 解析响应
  84. dic = json.loads(response.body.decode('utf-8'))
  85. if 'access_token' not in dic or 'refresh_token' not in dic:
  86. raise CredentialException(f"failed to refresh OAuth token: {response.body.decode('utf-8')}")
  87. # 更新令牌
  88. new_access_token = dic.get('access_token')
  89. new_refresh_token = dic.get('refresh_token')
  90. expires_in = dic.get('expires_in', 3600)
  91. new_access_token_expire = current_time + expires_in
  92. self._access_token = new_access_token
  93. self._refresh_token = new_refresh_token
  94. self._access_token_expire = new_access_token_expire
  95. async def _try_refresh_oauth_token_async(self) -> None:
  96. current_time = int(time.mktime(time.localtime()))
  97. # 构建刷新令牌请求
  98. r = urlparse(self._sign_in_url)
  99. tea_request = ph.get_new_request()
  100. tea_request.headers['host'] = r.hostname
  101. tea_request.port = r.port
  102. tea_request.protocol = r.scheme
  103. tea_request.method = 'POST'
  104. tea_request.pathname = '/v1/token'
  105. # 设置请求体
  106. body_data = {
  107. 'grant_type': 'refresh_token',
  108. 'refresh_token': self._refresh_token,
  109. 'client_id': self._client_id,
  110. 'Timestamp': ph.get_iso_8061_date()
  111. }
  112. tea_request.body = urlencode(body_data)
  113. tea_request.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  114. response = await TeaCore.async_do_action(tea_request, self._runtime_options)
  115. if response.status_code != 200:
  116. raise CredentialException(f"failed to refresh OAuth token, status code: {response.status_code}, response: {response.body.decode('utf-8')}")
  117. # 解析响应
  118. dic = json.loads(response.body.decode('utf-8'))
  119. if 'access_token' not in dic or 'refresh_token' not in dic:
  120. raise CredentialException(f"failed to refresh OAuth token: {response.body.decode('utf-8')}")
  121. # 更新令牌
  122. new_access_token = dic.get('access_token')
  123. new_refresh_token = dic.get('refresh_token')
  124. expires_in = dic.get('expires_in', 3600)
  125. new_access_token_expire = current_time + expires_in
  126. self._access_token = new_access_token
  127. self._refresh_token = new_refresh_token
  128. self._access_token_expire = new_access_token_expire
  129. def _refresh_credentials(self) -> RefreshResult[Credentials]:
  130. if self._refresh_token and (
  131. self._access_token is None or self._access_token_expire <= 0 or self._access_token_expire - int(
  132. time.mktime(time.localtime())) <= 1200):
  133. self._try_refresh_oauth_token()
  134. r = urlparse(self._sign_in_url)
  135. tea_request = ph.get_new_request()
  136. tea_request.headers['host'] = r.hostname
  137. tea_request.port = r.port
  138. tea_request.protocol = r.scheme
  139. tea_request.method = 'POST'
  140. tea_request.pathname = '/v1/exchange'
  141. tea_request.headers['Content-Type'] = 'application/json'
  142. tea_request.headers['Authorization'] = f'Bearer {self._access_token}'
  143. response = TeaCore.do_action(tea_request, self._runtime_options)
  144. if response.status_code != 200:
  145. raise CredentialException(
  146. f"error refreshing credentials from OAuth, http_code: {response.status_code}, result: {response.body.decode('utf-8')}")
  147. dic = json.loads(response.body.decode('utf-8'))
  148. if 'error' in dic:
  149. raise CredentialException(
  150. f"error retrieving credentials from OAuth result: {response.body.decode('utf-8')}")
  151. if 'AccessKeyId' not in dic or 'AccessKeySecret' not in dic or 'SecurityToken' not in dic:
  152. raise CredentialException(
  153. f"error retrieving credentials from OAuth result: {response.body.decode('utf-8')}")
  154. # 先转换为时间数组
  155. time_array = time.strptime(dic.get('Expiration'), '%Y-%m-%dT%H:%M:%SZ')
  156. # 转换为时间戳
  157. expiration = calendar.timegm(time_array)
  158. credentials = Credentials(
  159. access_key_id=dic.get('AccessKeyId'),
  160. access_key_secret=dic.get('AccessKeySecret'),
  161. security_token=dic.get('SecurityToken'),
  162. expiration=expiration,
  163. provider_name=self.get_provider_name()
  164. )
  165. # 调用令牌更新回调函数
  166. if self._token_update_callback:
  167. try:
  168. self._token_update_callback(
  169. self._refresh_token,
  170. self._access_token,
  171. credentials.get_access_key_id(),
  172. credentials.get_access_key_secret(),
  173. credentials.get_security_token(),
  174. self._access_token_expire,
  175. expiration
  176. )
  177. except Exception as e:
  178. log.warning(f'failed to update OAuth tokens in config file: {e}')
  179. return RefreshResult(value=credentials,
  180. stale_time=_get_stale_time(expiration))
  181. async def _refresh_credentials_async(self) -> RefreshResult[Credentials]:
  182. if self._refresh_token and (
  183. self._access_token is None or self._access_token_expire <= 0 or self._access_token_expire - int(
  184. time.mktime(time.localtime())) <= 1200):
  185. await self._try_refresh_oauth_token_async()
  186. r = urlparse(self._sign_in_url)
  187. tea_request = ph.get_new_request()
  188. tea_request.headers['host'] = r.hostname
  189. tea_request.port = r.port
  190. tea_request.protocol = r.scheme
  191. tea_request.method = 'POST'
  192. tea_request.pathname = '/v1/exchange'
  193. tea_request.headers['Content-Type'] = 'application/json'
  194. tea_request.headers['Authorization'] = f'Bearer {self._access_token}'
  195. response = await TeaCore.async_do_action(tea_request, self._runtime_options)
  196. if response.status_code != 200:
  197. raise CredentialException(
  198. f"error refreshing credentials from OAuth, http_code: {response.status_code}, result: {response.body.decode('utf-8')}")
  199. dic = json.loads(response.body.decode('utf-8'))
  200. if 'error' in dic:
  201. raise CredentialException(
  202. f"error retrieving credentials from OAuth result: {response.body.decode('utf-8')}")
  203. if 'AccessKeyId' not in dic or 'AccessKeySecret' not in dic or 'SecurityToken' not in dic:
  204. raise CredentialException(
  205. f"error retrieving credentials from OAuth result: {response.body.decode('utf-8')}")
  206. # 先转换为时间数组
  207. time_array = time.strptime(dic.get('Expiration'), '%Y-%m-%dT%H:%M:%SZ')
  208. # 转换为时间戳
  209. expiration = calendar.timegm(time_array)
  210. credentials = Credentials(
  211. access_key_id=dic.get('AccessKeyId'),
  212. access_key_secret=dic.get('AccessKeySecret'),
  213. security_token=dic.get('SecurityToken'),
  214. expiration=expiration,
  215. provider_name=self.get_provider_name()
  216. )
  217. if self._token_update_callback_async:
  218. try:
  219. await self._token_update_callback_async(
  220. self._refresh_token,
  221. self._access_token,
  222. credentials.get_access_key_id(),
  223. credentials.get_access_key_secret(),
  224. credentials.get_security_token(),
  225. self._access_token_expire,
  226. expiration
  227. )
  228. except Exception as e:
  229. log.warning(f'failed to update OAuth tokens in config file: {e}')
  230. return RefreshResult(value=credentials,
  231. stale_time=_get_stale_time(expiration))
  232. def _get_client_id(self) -> str:
  233. """获取客户端ID"""
  234. return self._client_id
  235. def get_provider_name(self) -> str:
  236. return 'oauth'