sts.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. # Copyright 2020 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. """OAuth 2.0 Token Exchange Spec.
  15. This module defines a token exchange utility based on the `OAuth 2.0 Token
  16. Exchange`_ spec. This will be mainly used to exchange external credentials
  17. for GCP access tokens in workload identity pools to access Google APIs.
  18. The implementation will support various types of client authentication as
  19. allowed in the spec.
  20. A deviation on the spec will be for additional Google specific options that
  21. cannot be easily mapped to parameters defined in the RFC.
  22. The returned dictionary response will be based on the `rfc8693 section 2.2.1`_
  23. spec JSON response.
  24. .. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
  25. .. _rfc8693 section 2.2.1: https://tools.ietf.org/html/rfc8693#section-2.2.1
  26. """
  27. import http.client as http_client
  28. import json
  29. import urllib
  30. from google.oauth2 import utils
  31. _URLENCODED_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
  32. class Client(utils.OAuthClientAuthHandler):
  33. """Implements the OAuth 2.0 token exchange spec based on
  34. https://tools.ietf.org/html/rfc8693.
  35. """
  36. def __init__(self, token_exchange_endpoint, client_authentication=None):
  37. """Initializes an STS client instance.
  38. Args:
  39. token_exchange_endpoint (str): The token exchange endpoint.
  40. client_authentication (Optional(google.oauth2.oauth2_utils.ClientAuthentication)):
  41. The optional OAuth client authentication credentials if available.
  42. """
  43. super(Client, self).__init__(client_authentication)
  44. self._token_exchange_endpoint = token_exchange_endpoint
  45. def _make_request(self, request, headers, request_body):
  46. # Initialize request headers.
  47. request_headers = _URLENCODED_HEADERS.copy()
  48. # Inject additional headers.
  49. if headers:
  50. for k, v in dict(headers).items():
  51. request_headers[k] = v
  52. # Apply OAuth client authentication.
  53. self.apply_client_authentication_options(request_headers, request_body)
  54. # Execute request.
  55. response = request(
  56. url=self._token_exchange_endpoint,
  57. method="POST",
  58. headers=request_headers,
  59. body=urllib.parse.urlencode(request_body).encode("utf-8"),
  60. )
  61. response_body = (
  62. response.data.decode("utf-8")
  63. if hasattr(response.data, "decode")
  64. else response.data
  65. )
  66. # If non-200 response received, translate to OAuthError exception.
  67. if response.status != http_client.OK:
  68. utils.handle_error_response(response_body)
  69. response_data = json.loads(response_body)
  70. # Return successful response.
  71. return response_data
  72. def exchange_token(
  73. self,
  74. request,
  75. grant_type,
  76. subject_token,
  77. subject_token_type,
  78. resource=None,
  79. audience=None,
  80. scopes=None,
  81. requested_token_type=None,
  82. actor_token=None,
  83. actor_token_type=None,
  84. additional_options=None,
  85. additional_headers=None,
  86. ):
  87. """Exchanges the provided token for another type of token based on the
  88. rfc8693 spec.
  89. Args:
  90. request (google.auth.transport.Request): A callable used to make
  91. HTTP requests.
  92. grant_type (str): The OAuth 2.0 token exchange grant type.
  93. subject_token (str): The OAuth 2.0 token exchange subject token.
  94. subject_token_type (str): The OAuth 2.0 token exchange subject token type.
  95. resource (Optional[str]): The optional OAuth 2.0 token exchange resource field.
  96. audience (Optional[str]): The optional OAuth 2.0 token exchange audience field.
  97. scopes (Optional[Sequence[str]]): The optional list of scopes to use.
  98. requested_token_type (Optional[str]): The optional OAuth 2.0 token exchange requested
  99. token type.
  100. actor_token (Optional[str]): The optional OAuth 2.0 token exchange actor token.
  101. actor_token_type (Optional[str]): The optional OAuth 2.0 token exchange actor token type.
  102. additional_options (Optional[Mapping[str, str]]): The optional additional
  103. non-standard Google specific options.
  104. additional_headers (Optional[Mapping[str, str]]): The optional additional
  105. headers to pass to the token exchange endpoint.
  106. Returns:
  107. Mapping[str, str]: The token exchange JSON-decoded response data containing
  108. the requested token and its expiration time.
  109. Raises:
  110. google.auth.exceptions.OAuthError: If the token endpoint returned
  111. an error.
  112. """
  113. # Initialize request body.
  114. request_body = {
  115. "grant_type": grant_type,
  116. "resource": resource,
  117. "audience": audience,
  118. "scope": " ".join(scopes or []),
  119. "requested_token_type": requested_token_type,
  120. "subject_token": subject_token,
  121. "subject_token_type": subject_token_type,
  122. "actor_token": actor_token,
  123. "actor_token_type": actor_token_type,
  124. "options": None,
  125. }
  126. # Add additional non-standard options.
  127. if additional_options:
  128. request_body["options"] = urllib.parse.quote(json.dumps(additional_options))
  129. # Remove empty fields in request body.
  130. for k, v in dict(request_body).items():
  131. if v is None or v == "":
  132. del request_body[k]
  133. return self._make_request(request, additional_headers, request_body)
  134. def refresh_token(self, request, refresh_token):
  135. """Exchanges a refresh token for an access token based on the
  136. RFC6749 spec.
  137. Args:
  138. request (google.auth.transport.Request): A callable used to make
  139. HTTP requests.
  140. subject_token (str): The OAuth 2.0 refresh token.
  141. """
  142. return self._make_request(
  143. request,
  144. None,
  145. {"grant_type": "refresh_token", "refresh_token": refresh_token},
  146. )