credentials.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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. from google.auth import _helpers
  20. from google.auth import credentials
  21. from google.auth import exceptions
  22. from google.auth import iam
  23. from google.auth import jwt
  24. from google.auth import metrics
  25. from google.auth.compute_engine import _metadata
  26. from google.oauth2 import _client
  27. class Credentials(
  28. credentials.Scoped,
  29. credentials.CredentialsWithQuotaProject,
  30. credentials.CredentialsWithUniverseDomain,
  31. ):
  32. """Compute Engine Credentials.
  33. These credentials use the Google Compute Engine metadata server to obtain
  34. OAuth 2.0 access tokens associated with the instance's service account,
  35. and are also used for Cloud Run, Flex and App Engine (except for the Python
  36. 2.7 runtime, which is supported only on older versions of this library).
  37. For more information about Compute Engine authentication, including how
  38. to configure scopes, see the `Compute Engine authentication
  39. documentation`_.
  40. .. note:: On Compute Engine the metadata server ignores requested scopes.
  41. On Cloud Run, Flex and App Engine the server honours requested scopes.
  42. .. _Compute Engine authentication documentation:
  43. https://cloud.google.com/compute/docs/authentication#using
  44. """
  45. def __init__(
  46. self,
  47. service_account_email="default",
  48. quota_project_id=None,
  49. scopes=None,
  50. default_scopes=None,
  51. universe_domain=None,
  52. ):
  53. """
  54. Args:
  55. service_account_email (str): The service account email to use, or
  56. 'default'. A Compute Engine instance may have multiple service
  57. accounts.
  58. quota_project_id (Optional[str]): The project ID used for quota and
  59. billing.
  60. scopes (Optional[Sequence[str]]): The list of scopes for the credentials.
  61. default_scopes (Optional[Sequence[str]]): Default scopes passed by a
  62. Google client library. Use 'scopes' for user-defined scopes.
  63. universe_domain (Optional[str]): The universe domain. If not
  64. provided or None, credential will attempt to fetch the value
  65. from metadata server. If metadata server doesn't have universe
  66. domain endpoint, then the default googleapis.com will be used.
  67. """
  68. super(Credentials, self).__init__()
  69. self._service_account_email = service_account_email
  70. self._quota_project_id = quota_project_id
  71. self._scopes = scopes
  72. self._default_scopes = default_scopes
  73. self._universe_domain_cached = False
  74. if universe_domain:
  75. self._universe_domain = universe_domain
  76. self._universe_domain_cached = True
  77. def _retrieve_info(self, request):
  78. """Retrieve information about the service account.
  79. Updates the scopes and retrieves the full service account email.
  80. Args:
  81. request (google.auth.transport.Request): The object used to make
  82. HTTP requests.
  83. """
  84. info = _metadata.get_service_account_info(
  85. request, service_account=self._service_account_email
  86. )
  87. self._service_account_email = info["email"]
  88. # Don't override scopes requested by the user.
  89. if self._scopes is None:
  90. self._scopes = info["scopes"]
  91. def _metric_header_for_usage(self):
  92. return metrics.CRED_TYPE_SA_MDS
  93. def refresh(self, request):
  94. """Refresh the access token and scopes.
  95. Args:
  96. request (google.auth.transport.Request): The object used to make
  97. HTTP requests.
  98. Raises:
  99. google.auth.exceptions.RefreshError: If the Compute Engine metadata
  100. service can't be reached if if the instance has not
  101. credentials.
  102. """
  103. scopes = self._scopes if self._scopes is not None else self._default_scopes
  104. try:
  105. self._retrieve_info(request)
  106. self.token, self.expiry = _metadata.get_service_account_token(
  107. request, service_account=self._service_account_email, scopes=scopes
  108. )
  109. except exceptions.TransportError as caught_exc:
  110. new_exc = exceptions.RefreshError(caught_exc)
  111. raise new_exc from caught_exc
  112. @property
  113. def service_account_email(self):
  114. """The service account email.
  115. .. note:: This is not guaranteed to be set until :meth:`refresh` has been
  116. called.
  117. """
  118. return self._service_account_email
  119. @property
  120. def requires_scopes(self):
  121. return not self._scopes
  122. @property
  123. def universe_domain(self):
  124. if self._universe_domain_cached:
  125. return self._universe_domain
  126. from google.auth.transport import requests as google_auth_requests
  127. self._universe_domain = _metadata.get_universe_domain(
  128. google_auth_requests.Request()
  129. )
  130. self._universe_domain_cached = True
  131. return self._universe_domain
  132. @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
  133. def with_quota_project(self, quota_project_id):
  134. creds = self.__class__(
  135. service_account_email=self._service_account_email,
  136. quota_project_id=quota_project_id,
  137. scopes=self._scopes,
  138. default_scopes=self._default_scopes,
  139. )
  140. creds._universe_domain = self._universe_domain
  141. creds._universe_domain_cached = self._universe_domain_cached
  142. return creds
  143. @_helpers.copy_docstring(credentials.Scoped)
  144. def with_scopes(self, scopes, default_scopes=None):
  145. # Compute Engine credentials can not be scoped (the metadata service
  146. # ignores the scopes parameter). App Engine, Cloud Run and Flex support
  147. # requesting scopes.
  148. creds = self.__class__(
  149. scopes=scopes,
  150. default_scopes=default_scopes,
  151. service_account_email=self._service_account_email,
  152. quota_project_id=self._quota_project_id,
  153. )
  154. creds._universe_domain = self._universe_domain
  155. creds._universe_domain_cached = self._universe_domain_cached
  156. return creds
  157. @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
  158. def with_universe_domain(self, universe_domain):
  159. return self.__class__(
  160. scopes=self._scopes,
  161. default_scopes=self._default_scopes,
  162. service_account_email=self._service_account_email,
  163. quota_project_id=self._quota_project_id,
  164. universe_domain=universe_domain,
  165. )
  166. _DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
  167. _DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
  168. class IDTokenCredentials(
  169. credentials.CredentialsWithQuotaProject,
  170. credentials.Signing,
  171. credentials.CredentialsWithTokenUri,
  172. ):
  173. """Open ID Connect ID Token-based service account credentials.
  174. These credentials relies on the default service account of a GCE instance.
  175. ID token can be requested from `GCE metadata server identity endpoint`_, IAM
  176. token endpoint or other token endpoints you specify. If metadata server
  177. identity endpoint is not used, the GCE instance must have been started with
  178. a service account that has access to the IAM Cloud API.
  179. .. _GCE metadata server identity endpoint:
  180. https://cloud.google.com/compute/docs/instances/verifying-instance-identity
  181. """
  182. def __init__(
  183. self,
  184. request,
  185. target_audience,
  186. token_uri=None,
  187. additional_claims=None,
  188. service_account_email=None,
  189. signer=None,
  190. use_metadata_identity_endpoint=False,
  191. quota_project_id=None,
  192. ):
  193. """
  194. Args:
  195. request (google.auth.transport.Request): The object used to make
  196. HTTP requests.
  197. target_audience (str): The intended audience for these credentials,
  198. used when requesting the ID Token. The ID Token's ``aud`` claim
  199. will be set to this string.
  200. token_uri (str): The OAuth 2.0 Token URI.
  201. additional_claims (Mapping[str, str]): Any additional claims for
  202. the JWT assertion used in the authorization grant.
  203. service_account_email (str): Optional explicit service account to
  204. use to sign JWT tokens.
  205. By default, this is the default GCE service account.
  206. signer (google.auth.crypt.Signer): The signer used to sign JWTs.
  207. In case the signer is specified, the request argument will be
  208. ignored.
  209. use_metadata_identity_endpoint (bool): Whether to use GCE metadata
  210. identity endpoint. For backward compatibility the default value
  211. is False. If set to True, ``token_uri``, ``additional_claims``,
  212. ``service_account_email``, ``signer`` argument should not be set;
  213. otherwise ValueError will be raised.
  214. quota_project_id (Optional[str]): The project ID used for quota and
  215. billing.
  216. Raises:
  217. ValueError:
  218. If ``use_metadata_identity_endpoint`` is set to True, and one of
  219. ``token_uri``, ``additional_claims``, ``service_account_email``,
  220. ``signer`` arguments is set.
  221. """
  222. super(IDTokenCredentials, self).__init__()
  223. self._quota_project_id = quota_project_id
  224. self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
  225. self._target_audience = target_audience
  226. if use_metadata_identity_endpoint:
  227. if token_uri or additional_claims or service_account_email or signer:
  228. raise exceptions.MalformedError(
  229. "If use_metadata_identity_endpoint is set, token_uri, "
  230. "additional_claims, service_account_email, signer arguments"
  231. " must not be set"
  232. )
  233. self._token_uri = None
  234. self._additional_claims = None
  235. self._signer = None
  236. if service_account_email is None:
  237. sa_info = _metadata.get_service_account_info(request)
  238. self._service_account_email = sa_info["email"]
  239. else:
  240. self._service_account_email = service_account_email
  241. if not use_metadata_identity_endpoint:
  242. if signer is None:
  243. signer = iam.Signer(
  244. request=request,
  245. credentials=Credentials(),
  246. service_account_email=self._service_account_email,
  247. )
  248. self._signer = signer
  249. self._token_uri = token_uri or _DEFAULT_TOKEN_URI
  250. if additional_claims is not None:
  251. self._additional_claims = additional_claims
  252. else:
  253. self._additional_claims = {}
  254. def with_target_audience(self, target_audience):
  255. """Create a copy of these credentials with the specified target
  256. audience.
  257. Args:
  258. target_audience (str): The intended audience for these credentials,
  259. used when requesting the ID Token.
  260. Returns:
  261. google.auth.service_account.IDTokenCredentials: A new credentials
  262. instance.
  263. """
  264. # since the signer is already instantiated,
  265. # the request is not needed
  266. if self._use_metadata_identity_endpoint:
  267. return self.__class__(
  268. None,
  269. target_audience=target_audience,
  270. use_metadata_identity_endpoint=True,
  271. quota_project_id=self._quota_project_id,
  272. )
  273. else:
  274. return self.__class__(
  275. None,
  276. service_account_email=self._service_account_email,
  277. token_uri=self._token_uri,
  278. target_audience=target_audience,
  279. additional_claims=self._additional_claims.copy(),
  280. signer=self.signer,
  281. use_metadata_identity_endpoint=False,
  282. quota_project_id=self._quota_project_id,
  283. )
  284. @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
  285. def with_quota_project(self, quota_project_id):
  286. # since the signer is already instantiated,
  287. # the request is not needed
  288. if self._use_metadata_identity_endpoint:
  289. return self.__class__(
  290. None,
  291. target_audience=self._target_audience,
  292. use_metadata_identity_endpoint=True,
  293. quota_project_id=quota_project_id,
  294. )
  295. else:
  296. return self.__class__(
  297. None,
  298. service_account_email=self._service_account_email,
  299. token_uri=self._token_uri,
  300. target_audience=self._target_audience,
  301. additional_claims=self._additional_claims.copy(),
  302. signer=self.signer,
  303. use_metadata_identity_endpoint=False,
  304. quota_project_id=quota_project_id,
  305. )
  306. @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
  307. def with_token_uri(self, token_uri):
  308. # since the signer is already instantiated,
  309. # the request is not needed
  310. if self._use_metadata_identity_endpoint:
  311. raise exceptions.MalformedError(
  312. "If use_metadata_identity_endpoint is set, token_uri" " must not be set"
  313. )
  314. else:
  315. return self.__class__(
  316. None,
  317. service_account_email=self._service_account_email,
  318. token_uri=token_uri,
  319. target_audience=self._target_audience,
  320. additional_claims=self._additional_claims.copy(),
  321. signer=self.signer,
  322. use_metadata_identity_endpoint=False,
  323. quota_project_id=self.quota_project_id,
  324. )
  325. def _make_authorization_grant_assertion(self):
  326. """Create the OAuth 2.0 assertion.
  327. This assertion is used during the OAuth 2.0 grant to acquire an
  328. ID token.
  329. Returns:
  330. bytes: The authorization grant assertion.
  331. """
  332. now = _helpers.utcnow()
  333. lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
  334. expiry = now + lifetime
  335. payload = {
  336. "iat": _helpers.datetime_to_secs(now),
  337. "exp": _helpers.datetime_to_secs(expiry),
  338. # The issuer must be the service account email.
  339. "iss": self.service_account_email,
  340. # The audience must be the auth token endpoint's URI
  341. "aud": self._token_uri,
  342. # The target audience specifies which service the ID token is
  343. # intended for.
  344. "target_audience": self._target_audience,
  345. }
  346. payload.update(self._additional_claims)
  347. token = jwt.encode(self._signer, payload)
  348. return token
  349. def _call_metadata_identity_endpoint(self, request):
  350. """Request ID token from metadata identity endpoint.
  351. Args:
  352. request (google.auth.transport.Request): The object used to make
  353. HTTP requests.
  354. Returns:
  355. Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token.
  356. Raises:
  357. google.auth.exceptions.RefreshError: If the Compute Engine metadata
  358. service can't be reached or if the instance has no credentials.
  359. ValueError: If extracting expiry from the obtained ID token fails.
  360. """
  361. try:
  362. path = "instance/service-accounts/default/identity"
  363. params = {"audience": self._target_audience, "format": "full"}
  364. metrics_header = {
  365. metrics.API_CLIENT_HEADER: metrics.token_request_id_token_mds()
  366. }
  367. id_token = _metadata.get(
  368. request, path, params=params, headers=metrics_header
  369. )
  370. except exceptions.TransportError as caught_exc:
  371. new_exc = exceptions.RefreshError(caught_exc)
  372. raise new_exc from caught_exc
  373. _, payload, _, _ = jwt._unverified_decode(id_token)
  374. return id_token, datetime.datetime.utcfromtimestamp(payload["exp"])
  375. def refresh(self, request):
  376. """Refreshes the ID token.
  377. Args:
  378. request (google.auth.transport.Request): The object used to make
  379. HTTP requests.
  380. Raises:
  381. google.auth.exceptions.RefreshError: If the credentials could
  382. not be refreshed.
  383. ValueError: If extracting expiry from the obtained ID token fails.
  384. """
  385. if self._use_metadata_identity_endpoint:
  386. self.token, self.expiry = self._call_metadata_identity_endpoint(request)
  387. else:
  388. assertion = self._make_authorization_grant_assertion()
  389. access_token, expiry, _ = _client.id_token_jwt_grant(
  390. request, self._token_uri, assertion
  391. )
  392. self.token = access_token
  393. self.expiry = expiry
  394. @property # type: ignore
  395. @_helpers.copy_docstring(credentials.Signing)
  396. def signer(self):
  397. return self._signer
  398. def sign_bytes(self, message):
  399. """Signs the given message.
  400. Args:
  401. message (bytes): The message to sign.
  402. Returns:
  403. bytes: The message's cryptographic signature.
  404. Raises:
  405. ValueError:
  406. Signer is not available if metadata identity endpoint is used.
  407. """
  408. if self._use_metadata_identity_endpoint:
  409. raise exceptions.InvalidOperation(
  410. "Signer is not available if metadata identity endpoint is used"
  411. )
  412. return self._signer.sign(message)
  413. @property
  414. def service_account_email(self):
  415. """The service account email."""
  416. return self._service_account_email
  417. @property
  418. def signer_email(self):
  419. return self._service_account_email