credentials.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. # Copyright 2016 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Google Compute Engine credentials.
  15. This module provides authentication for an application running on Google
  16. Compute Engine using the Compute Engine metadata server.
  17. """
  18. import datetime
  19. import six
  20. from google.auth import _helpers
  21. from google.auth import credentials
  22. from google.auth import exceptions
  23. from google.auth import iam
  24. from google.auth import jwt
  25. from google.auth.compute_engine import _metadata
  26. from google.oauth2 import _client
  27. class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject):
  28. """Compute Engine Credentials.
  29. These credentials use the Google Compute Engine metadata server to obtain
  30. OAuth 2.0 access tokens associated with the instance's service account,
  31. and are also used for Cloud Run, Flex and App Engine (except for the Python
  32. 2.7 runtime).
  33. For more information about Compute Engine authentication, including how
  34. to configure scopes, see the `Compute Engine authentication
  35. documentation`_.
  36. .. note:: On Compute Engine the metadata server ignores requested scopes.
  37. On Cloud Run, Flex and App Engine the server honours requested scopes.
  38. .. _Compute Engine authentication documentation:
  39. https://cloud.google.com/compute/docs/authentication#using
  40. """
  41. def __init__(
  42. self,
  43. service_account_email="default",
  44. quota_project_id=None,
  45. scopes=None,
  46. default_scopes=None,
  47. ):
  48. """
  49. Args:
  50. service_account_email (str): The service account email to use, or
  51. 'default'. A Compute Engine instance may have multiple service
  52. accounts.
  53. quota_project_id (Optional[str]): The project ID used for quota and
  54. billing.
  55. scopes (Optional[Sequence[str]]): The list of scopes for the credentials.
  56. default_scopes (Optional[Sequence[str]]): Default scopes passed by a
  57. Google client library. Use 'scopes' for user-defined scopes.
  58. """
  59. super(Credentials, self).__init__()
  60. self._service_account_email = service_account_email
  61. self._quota_project_id = quota_project_id
  62. self._scopes = scopes
  63. self._default_scopes = default_scopes
  64. def _retrieve_info(self, request):
  65. """Retrieve information about the service account.
  66. Updates the scopes and retrieves the full service account email.
  67. Args:
  68. request (google.auth.transport.Request): The object used to make
  69. HTTP requests.
  70. """
  71. info = _metadata.get_service_account_info(
  72. request, service_account=self._service_account_email
  73. )
  74. self._service_account_email = info["email"]
  75. # Don't override scopes requested by the user.
  76. if self._scopes is None:
  77. self._scopes = info["scopes"]
  78. def refresh(self, request):
  79. """Refresh the access token and scopes.
  80. Args:
  81. request (google.auth.transport.Request): The object used to make
  82. HTTP requests.
  83. Raises:
  84. google.auth.exceptions.RefreshError: If the Compute Engine metadata
  85. service can't be reached if if the instance has not
  86. credentials.
  87. """
  88. scopes = self._scopes if self._scopes is not None else self._default_scopes
  89. try:
  90. self._retrieve_info(request)
  91. self.token, self.expiry = _metadata.get_service_account_token(
  92. request, service_account=self._service_account_email, scopes=scopes
  93. )
  94. except exceptions.TransportError as caught_exc:
  95. new_exc = exceptions.RefreshError(caught_exc)
  96. six.raise_from(new_exc, caught_exc)
  97. @property
  98. def service_account_email(self):
  99. """The service account email.
  100. .. note:: This is not guaranteed to be set until :meth:`refresh` has been
  101. called.
  102. """
  103. return self._service_account_email
  104. @property
  105. def requires_scopes(self):
  106. return not self._scopes
  107. @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
  108. def with_quota_project(self, quota_project_id):
  109. return self.__class__(
  110. service_account_email=self._service_account_email,
  111. quota_project_id=quota_project_id,
  112. scopes=self._scopes,
  113. )
  114. @_helpers.copy_docstring(credentials.Scoped)
  115. def with_scopes(self, scopes, default_scopes=None):
  116. # Compute Engine credentials can not be scoped (the metadata service
  117. # ignores the scopes parameter). App Engine, Cloud Run and Flex support
  118. # requesting scopes.
  119. return self.__class__(
  120. scopes=scopes,
  121. default_scopes=default_scopes,
  122. service_account_email=self._service_account_email,
  123. quota_project_id=self._quota_project_id,
  124. )
  125. _DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
  126. _DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
  127. class IDTokenCredentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
  128. """Open ID Connect ID Token-based service account credentials.
  129. These credentials relies on the default service account of a GCE instance.
  130. ID token can be requested from `GCE metadata server identity endpoint`_, IAM
  131. token endpoint or other token endpoints you specify. If metadata server
  132. identity endpoint is not used, the GCE instance must have been started with
  133. a service account that has access to the IAM Cloud API.
  134. .. _GCE metadata server identity endpoint:
  135. https://cloud.google.com/compute/docs/instances/verifying-instance-identity
  136. """
  137. def __init__(
  138. self,
  139. request,
  140. target_audience,
  141. token_uri=None,
  142. additional_claims=None,
  143. service_account_email=None,
  144. signer=None,
  145. use_metadata_identity_endpoint=False,
  146. quota_project_id=None,
  147. ):
  148. """
  149. Args:
  150. request (google.auth.transport.Request): The object used to make
  151. HTTP requests.
  152. target_audience (str): The intended audience for these credentials,
  153. used when requesting the ID Token. The ID Token's ``aud`` claim
  154. will be set to this string.
  155. token_uri (str): The OAuth 2.0 Token URI.
  156. additional_claims (Mapping[str, str]): Any additional claims for
  157. the JWT assertion used in the authorization grant.
  158. service_account_email (str): Optional explicit service account to
  159. use to sign JWT tokens.
  160. By default, this is the default GCE service account.
  161. signer (google.auth.crypt.Signer): The signer used to sign JWTs.
  162. In case the signer is specified, the request argument will be
  163. ignored.
  164. use_metadata_identity_endpoint (bool): Whether to use GCE metadata
  165. identity endpoint. For backward compatibility the default value
  166. is False. If set to True, ``token_uri``, ``additional_claims``,
  167. ``service_account_email``, ``signer`` argument should not be set;
  168. otherwise ValueError will be raised.
  169. quota_project_id (Optional[str]): The project ID used for quota and
  170. billing.
  171. Raises:
  172. ValueError:
  173. If ``use_metadata_identity_endpoint`` is set to True, and one of
  174. ``token_uri``, ``additional_claims``, ``service_account_email``,
  175. ``signer`` arguments is set.
  176. """
  177. super(IDTokenCredentials, self).__init__()
  178. self._quota_project_id = quota_project_id
  179. self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
  180. self._target_audience = target_audience
  181. if use_metadata_identity_endpoint:
  182. if token_uri or additional_claims or service_account_email or signer:
  183. raise ValueError(
  184. "If use_metadata_identity_endpoint is set, token_uri, "
  185. "additional_claims, service_account_email, signer arguments"
  186. " must not be set"
  187. )
  188. self._token_uri = None
  189. self._additional_claims = None
  190. self._signer = None
  191. if service_account_email is None:
  192. sa_info = _metadata.get_service_account_info(request)
  193. self._service_account_email = sa_info["email"]
  194. else:
  195. self._service_account_email = service_account_email
  196. if not use_metadata_identity_endpoint:
  197. if signer is None:
  198. signer = iam.Signer(
  199. request=request,
  200. credentials=Credentials(),
  201. service_account_email=self._service_account_email,
  202. )
  203. self._signer = signer
  204. self._token_uri = token_uri or _DEFAULT_TOKEN_URI
  205. if additional_claims is not None:
  206. self._additional_claims = additional_claims
  207. else:
  208. self._additional_claims = {}
  209. def with_target_audience(self, target_audience):
  210. """Create a copy of these credentials with the specified target
  211. audience.
  212. Args:
  213. target_audience (str): The intended audience for these credentials,
  214. used when requesting the ID Token.
  215. Returns:
  216. google.auth.service_account.IDTokenCredentials: A new credentials
  217. instance.
  218. """
  219. # since the signer is already instantiated,
  220. # the request is not needed
  221. if self._use_metadata_identity_endpoint:
  222. return self.__class__(
  223. None,
  224. target_audience=target_audience,
  225. use_metadata_identity_endpoint=True,
  226. quota_project_id=self._quota_project_id,
  227. )
  228. else:
  229. return self.__class__(
  230. None,
  231. service_account_email=self._service_account_email,
  232. token_uri=self._token_uri,
  233. target_audience=target_audience,
  234. additional_claims=self._additional_claims.copy(),
  235. signer=self.signer,
  236. use_metadata_identity_endpoint=False,
  237. quota_project_id=self._quota_project_id,
  238. )
  239. @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
  240. def with_quota_project(self, quota_project_id):
  241. # since the signer is already instantiated,
  242. # the request is not needed
  243. if self._use_metadata_identity_endpoint:
  244. return self.__class__(
  245. None,
  246. target_audience=self._target_audience,
  247. use_metadata_identity_endpoint=True,
  248. quota_project_id=quota_project_id,
  249. )
  250. else:
  251. return self.__class__(
  252. None,
  253. service_account_email=self._service_account_email,
  254. token_uri=self._token_uri,
  255. target_audience=self._target_audience,
  256. additional_claims=self._additional_claims.copy(),
  257. signer=self.signer,
  258. use_metadata_identity_endpoint=False,
  259. quota_project_id=quota_project_id,
  260. )
  261. def _make_authorization_grant_assertion(self):
  262. """Create the OAuth 2.0 assertion.
  263. This assertion is used during the OAuth 2.0 grant to acquire an
  264. ID token.
  265. Returns:
  266. bytes: The authorization grant assertion.
  267. """
  268. now = _helpers.utcnow()
  269. lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
  270. expiry = now + lifetime
  271. payload = {
  272. "iat": _helpers.datetime_to_secs(now),
  273. "exp": _helpers.datetime_to_secs(expiry),
  274. # The issuer must be the service account email.
  275. "iss": self.service_account_email,
  276. # The audience must be the auth token endpoint's URI
  277. "aud": self._token_uri,
  278. # The target audience specifies which service the ID token is
  279. # intended for.
  280. "target_audience": self._target_audience,
  281. }
  282. payload.update(self._additional_claims)
  283. token = jwt.encode(self._signer, payload)
  284. return token
  285. def _call_metadata_identity_endpoint(self, request):
  286. """Request ID token from metadata identity endpoint.
  287. Args:
  288. request (google.auth.transport.Request): The object used to make
  289. HTTP requests.
  290. Returns:
  291. Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token.
  292. Raises:
  293. google.auth.exceptions.RefreshError: If the Compute Engine metadata
  294. service can't be reached or if the instance has no credentials.
  295. ValueError: If extracting expiry from the obtained ID token fails.
  296. """
  297. try:
  298. path = "instance/service-accounts/default/identity"
  299. params = {"audience": self._target_audience, "format": "full"}
  300. id_token = _metadata.get(request, path, params=params)
  301. except exceptions.TransportError as caught_exc:
  302. new_exc = exceptions.RefreshError(caught_exc)
  303. six.raise_from(new_exc, caught_exc)
  304. _, payload, _, _ = jwt._unverified_decode(id_token)
  305. return id_token, datetime.datetime.fromtimestamp(payload["exp"])
  306. def refresh(self, request):
  307. """Refreshes the ID token.
  308. Args:
  309. request (google.auth.transport.Request): The object used to make
  310. HTTP requests.
  311. Raises:
  312. google.auth.exceptions.RefreshError: If the credentials could
  313. not be refreshed.
  314. ValueError: If extracting expiry from the obtained ID token fails.
  315. """
  316. if self._use_metadata_identity_endpoint:
  317. self.token, self.expiry = self._call_metadata_identity_endpoint(request)
  318. else:
  319. assertion = self._make_authorization_grant_assertion()
  320. access_token, expiry, _ = _client.id_token_jwt_grant(
  321. request, self._token_uri, assertion
  322. )
  323. self.token = access_token
  324. self.expiry = expiry
  325. @property
  326. @_helpers.copy_docstring(credentials.Signing)
  327. def signer(self):
  328. return self._signer
  329. def sign_bytes(self, message):
  330. """Signs the given message.
  331. Args:
  332. message (bytes): The message to sign.
  333. Returns:
  334. bytes: The message's cryptographic signature.
  335. Raises:
  336. ValueError:
  337. Signer is not available if metadata identity endpoint is used.
  338. """
  339. if self._use_metadata_identity_endpoint:
  340. raise ValueError(
  341. "Signer is not available if metadata identity endpoint is used"
  342. )
  343. return self._signer.sign(message)
  344. @property
  345. def service_account_email(self):
  346. """The service account email."""
  347. return self._service_account_email
  348. @property
  349. def signer_email(self):
  350. return self._service_account_email