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