blob: 9f2d68af3cab6e32ccb62ac3a6cc8d3f8f4e3506 [file] [log] [blame]
bojeil-googled4d7f382021-02-16 12:33:20 -08001# 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
Tres Seaver560cf1e2021-08-03 16:35:54 -040034import http.client
bojeil-googled4d7f382021-02-16 12:33:20 -080035import json
Tres Seaver560cf1e2021-08-03 16:35:54 -040036import urllib
bojeil-googled4d7f382021-02-16 12:33:20 -080037
38from google.oauth2 import utils
39
40
41_URLENCODED_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
42
43
44class Client(utils.OAuthClientAuthHandler):
45 """Implements the OAuth 2.0 token exchange spec based on
46 https://tools.ietf.org/html/rfc8693.
47 """
48
49 def __init__(self, token_exchange_endpoint, client_authentication=None):
50 """Initializes an STS client instance.
51
52 Args:
53 token_exchange_endpoint (str): The token exchange endpoint.
54 client_authentication (Optional(google.oauth2.oauth2_utils.ClientAuthentication)):
55 The optional OAuth client authentication credentials if available.
56 """
57 super(Client, self).__init__(client_authentication)
58 self._token_exchange_endpoint = token_exchange_endpoint
59
60 def exchange_token(
61 self,
62 request,
63 grant_type,
64 subject_token,
65 subject_token_type,
66 resource=None,
67 audience=None,
68 scopes=None,
69 requested_token_type=None,
70 actor_token=None,
71 actor_token_type=None,
72 additional_options=None,
73 additional_headers=None,
74 ):
75 """Exchanges the provided token for another type of token based on the
76 rfc8693 spec.
77
78 Args:
79 request (google.auth.transport.Request): A callable used to make
80 HTTP requests.
81 grant_type (str): The OAuth 2.0 token exchange grant type.
82 subject_token (str): The OAuth 2.0 token exchange subject token.
83 subject_token_type (str): The OAuth 2.0 token exchange subject token type.
84 resource (Optional[str]): The optional OAuth 2.0 token exchange resource field.
85 audience (Optional[str]): The optional OAuth 2.0 token exchange audience field.
86 scopes (Optional[Sequence[str]]): The optional list of scopes to use.
87 requested_token_type (Optional[str]): The optional OAuth 2.0 token exchange requested
88 token type.
89 actor_token (Optional[str]): The optional OAuth 2.0 token exchange actor token.
90 actor_token_type (Optional[str]): The optional OAuth 2.0 token exchange actor token type.
91 additional_options (Optional[Mapping[str, str]]): The optional additional
92 non-standard Google specific options.
93 additional_headers (Optional[Mapping[str, str]]): The optional additional
94 headers to pass to the token exchange endpoint.
95
96 Returns:
97 Mapping[str, str]: The token exchange JSON-decoded response data containing
98 the requested token and its expiration time.
99
100 Raises:
101 google.auth.exceptions.OAuthError: If the token endpoint returned
102 an error.
103 """
104 # Initialize request headers.
105 headers = _URLENCODED_HEADERS.copy()
106 # Inject additional headers.
107 if additional_headers:
108 for k, v in dict(additional_headers).items():
109 headers[k] = v
110 # Initialize request body.
111 request_body = {
112 "grant_type": grant_type,
113 "resource": resource,
114 "audience": audience,
115 "scope": " ".join(scopes or []),
116 "requested_token_type": requested_token_type,
117 "subject_token": subject_token,
118 "subject_token_type": subject_token_type,
119 "actor_token": actor_token,
120 "actor_token_type": actor_token_type,
121 "options": None,
122 }
123 # Add additional non-standard options.
124 if additional_options:
125 request_body["options"] = urllib.parse.quote(json.dumps(additional_options))
126 # Remove empty fields in request body.
127 for k, v in dict(request_body).items():
128 if v is None or v == "":
129 del request_body[k]
130 # Apply OAuth client authentication.
131 self.apply_client_authentication_options(headers, request_body)
132
133 # Execute request.
134 response = request(
135 url=self._token_exchange_endpoint,
136 method="POST",
137 headers=headers,
138 body=urllib.parse.urlencode(request_body).encode("utf-8"),
139 )
140
141 response_body = (
142 response.data.decode("utf-8")
143 if hasattr(response.data, "decode")
144 else response.data
145 )
146
147 # If non-200 response received, translate to OAuthError exception.
Tres Seaver560cf1e2021-08-03 16:35:54 -0400148 if response.status != http.client.OK:
bojeil-googled4d7f382021-02-16 12:33:20 -0800149 utils.handle_error_response(response_body)
150
151 response_data = json.loads(response_body)
152
153 # Return successful response.
154 return response_data