blob: 1e496d12ec7a66f1e90fcdd3e4385c0b3a8f9628 [file] [log] [blame]
arithmetic172882293fe2021-04-14 11:22:13 -07001# Copyright 2021 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"""A module that provides functions for handling rapt authentication.
16
17Reauth is a process of obtaining additional authentication (such as password,
18security token, etc.) while refreshing OAuth 2.0 credentials for a user.
19
20Credentials that use the Reauth flow must have the reauth scope,
21``https://www.googleapis.com/auth/accounts.reauth``.
22
23This module provides a high-level function for executing the Reauth process,
24:func:`refresh_grant`, and lower-level helpers for doing the individual
25steps of the reauth process.
26
27Those steps are:
28
291. Obtaining a list of challenges from the reauth server.
302. Running through each challenge and sending the result back to the reauth
31 server.
323. Refreshing the access token using the returned rapt token.
33"""
34
35import sys
36
arithmetic172882293fe2021-04-14 11:22:13 -070037from google.auth import exceptions
38from google.oauth2 import _client
39from google.oauth2 import challenges
40
41
42_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"
43_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"
44
45_REAUTH_NEEDED_ERROR = "invalid_grant"
46_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"
47_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"
48
49_AUTHENTICATED = "AUTHENTICATED"
50_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"
51_CHALLENGE_PENDING = "CHALLENGE_PENDING"
52
53
54# Override this global variable to set custom max number of rounds of reauth
55# challenges should be run.
56RUN_CHALLENGE_RETRY_LIMIT = 5
57
58
59def is_interactive():
60 """Check if we are in an interractive environment.
61
62 Override this function with a different logic if you are using this library
63 outside a CLI.
64
65 If the rapt token needs refreshing, the user needs to answer the challenges.
66 If the user is not in an interractive environment, the challenges can not
67 be answered and we just wait for timeout for no reason.
68
69 Returns:
70 bool: True if is interactive environment, False otherwise.
71 """
72
73 return sys.stdin.isatty()
74
75
76def _get_challenges(
77 request, supported_challenge_types, access_token, requested_scopes=None
78):
79 """Does initial request to reauth API to get the challenges.
80
81 Args:
82 request (google.auth.transport.Request): A callable used to make
83 HTTP requests.
84 supported_challenge_types (Sequence[str]): list of challenge names
85 supported by the manager.
86 access_token (str): Access token with reauth scopes.
87 requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
88
89 Returns:
90 dict: The response from the reauth API.
91 """
92 body = {"supportedChallengeTypes": supported_challenge_types}
93 if requested_scopes:
94 body["oauthScopesForDomainPolicyLookup"] = requested_scopes
95
96 return _client._token_endpoint_request(
97 request, _REAUTH_API + ":start", body, access_token=access_token, use_json=True
98 )
99
100
101def _send_challenge_result(
102 request, session_id, challenge_id, client_input, access_token
103):
104 """Attempt to refresh access token by sending next challenge result.
105
106 Args:
107 request (google.auth.transport.Request): A callable used to make
108 HTTP requests.
109 session_id (str): session id returned by the initial reauth call.
110 challenge_id (str): challenge id returned by the initial reauth call.
111 client_input: dict with a challenge-specific client input. For example:
112 ``{'credential': password}`` for password challenge.
113 access_token (str): Access token with reauth scopes.
114
115 Returns:
116 dict: The response from the reauth API.
117 """
118 body = {
119 "sessionId": session_id,
120 "challengeId": challenge_id,
121 "action": "RESPOND",
122 "proposalResponse": client_input,
123 }
124
125 return _client._token_endpoint_request(
126 request,
127 _REAUTH_API + "/{}:continue".format(session_id),
128 body,
129 access_token=access_token,
130 use_json=True,
131 )
132
133
134def _run_next_challenge(msg, request, access_token):
135 """Get the next challenge from msg and run it.
136
137 Args:
138 msg (dict): Reauth API response body (either from the initial request to
139 https://reauth.googleapis.com/v2/sessions:start or from sending the
140 previous challenge response to
141 https://reauth.googleapis.com/v2/sessions/id:continue)
142 request (google.auth.transport.Request): A callable used to make
143 HTTP requests.
144 access_token (str): reauth access token
145
146 Returns:
147 dict: The response from the reauth API.
148
149 Raises:
150 google.auth.exceptions.ReauthError: if reauth failed.
151 """
152 for challenge in msg["challenges"]:
153 if challenge["status"] != "READY":
154 # Skip non-activated challenges.
155 continue
156 c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
157 if not c:
158 raise exceptions.ReauthFailError(
159 "Unsupported challenge type {0}. Supported types: {1}".format(
160 challenge["challengeType"],
161 ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
162 )
163 )
164 if not c.is_locally_eligible:
165 raise exceptions.ReauthFailError(
166 "Challenge {0} is not locally eligible".format(
167 challenge["challengeType"]
168 )
169 )
170 client_input = c.obtain_challenge_input(challenge)
171 if not client_input:
172 return None
173 return _send_challenge_result(
174 request,
175 msg["sessionId"],
176 challenge["challengeId"],
177 client_input,
178 access_token,
179 )
180 return None
181
182
183def _obtain_rapt(request, access_token, requested_scopes):
184 """Given an http request method and reauth access token, get rapt token.
185
186 Args:
187 request (google.auth.transport.Request): A callable used to make
188 HTTP requests.
189 access_token (str): reauth access token
190 requested_scopes (Sequence[str]): scopes required by the client application
191
192 Returns:
193 str: The rapt token.
194
195 Raises:
196 google.auth.exceptions.ReauthError: if reauth failed
197 """
198 msg = _get_challenges(
199 request,
200 list(challenges.AVAILABLE_CHALLENGES.keys()),
201 access_token,
202 requested_scopes,
203 )
204
205 if msg["status"] == _AUTHENTICATED:
206 return msg["encodedProofOfReauthToken"]
207
208 for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):
209 if not (
210 msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING
211 ):
212 raise exceptions.ReauthFailError(
213 "Reauthentication challenge failed due to API error: {}".format(
214 msg["status"]
215 )
216 )
217
218 if not is_interactive():
219 raise exceptions.ReauthFailError(
220 "Reauthentication challenge could not be answered because you are not"
221 " in an interactive session."
222 )
223
224 msg = _run_next_challenge(msg, request, access_token)
225
226 if msg["status"] == _AUTHENTICATED:
227 return msg["encodedProofOfReauthToken"]
228
229 # If we got here it means we didn't get authenticated.
230 raise exceptions.ReauthFailError("Failed to obtain rapt token.")
231
232
233def get_rapt_token(
234 request, client_id, client_secret, refresh_token, token_uri, scopes=None
235):
236 """Given an http request method and refresh_token, get rapt token.
237
238 Args:
239 request (google.auth.transport.Request): A callable used to make
240 HTTP requests.
241 client_id (str): client id to get access token for reauth scope.
242 client_secret (str): client secret for the client_id
243 refresh_token (str): refresh token to refresh access token
244 token_uri (str): uri to refresh access token
245 scopes (Optional(Sequence[str])): scopes required by the client application
246
247 Returns:
248 str: The rapt token.
249 Raises:
250 google.auth.exceptions.RefreshError: If reauth failed.
251 """
252 sys.stderr.write("Reauthentication required.\n")
253
254 # Get access token for reauth.
255 access_token, _, _, _ = _client.refresh_grant(
256 request=request,
257 client_id=client_id,
258 client_secret=client_secret,
259 refresh_token=refresh_token,
260 token_uri=token_uri,
261 scopes=[_REAUTH_SCOPE],
262 )
263
264 # Get rapt token from reauth API.
265 rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
266
267 return rapt_token
268
269
270def refresh_grant(
271 request,
272 token_uri,
273 refresh_token,
274 client_id,
275 client_secret,
276 scopes=None,
277 rapt_token=None,
arithmetic172813aed5f2021-09-07 16:24:45 -0700278 enable_reauth_refresh=False,
arithmetic172882293fe2021-04-14 11:22:13 -0700279):
280 """Implements the reauthentication flow.
281
282 Args:
283 request (google.auth.transport.Request): A callable used to make
284 HTTP requests.
285 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
286 URI.
287 refresh_token (str): The refresh token to use to get a new access
288 token.
289 client_id (str): The OAuth 2.0 application's client ID.
290 client_secret (str): The Oauth 2.0 appliaction's client secret.
291 scopes (Optional(Sequence[str])): Scopes to request. If present, all
292 scopes must be authorized for the refresh token. Useful if refresh
293 token has a wild card scope (e.g.
294 'https://www.googleapis.com/auth/any-api').
295 rapt_token (Optional(str)): The rapt token for reauth.
arithmetic172813aed5f2021-09-07 16:24:45 -0700296 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
297 should be used. The default value is False. This option is for
298 gcloud only, other users should use the default value.
arithmetic172882293fe2021-04-14 11:22:13 -0700299
300 Returns:
arithmetic17289e108232021-04-23 15:27:02 -0700301 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
302 access token, new refresh token, expiration, the additional data
303 returned by the token endpoint, and the rapt token.
arithmetic172882293fe2021-04-14 11:22:13 -0700304
305 Raises:
306 google.auth.exceptions.RefreshError: If the token endpoint returned
307 an error.
308 """
309 body = {
310 "grant_type": _client._REFRESH_GRANT_TYPE,
311 "client_id": client_id,
312 "client_secret": client_secret,
313 "refresh_token": refresh_token,
314 }
315 if scopes:
316 body["scope"] = " ".join(scopes)
317 if rapt_token:
318 body["rapt"] = rapt_token
319
320 response_status_ok, response_data = _client._token_endpoint_request_no_throw(
321 request, token_uri, body
322 )
323 if (
324 not response_status_ok
325 and response_data.get("error") == _REAUTH_NEEDED_ERROR
326 and (
327 response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
328 or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
329 )
330 ):
arithmetic172813aed5f2021-09-07 16:24:45 -0700331 if not enable_reauth_refresh:
332 raise exceptions.RefreshError(
333 "Reauthentication is needed. Please run `gcloud auth login --update-adc` to reauthenticate."
334 )
335
arithmetic172882293fe2021-04-14 11:22:13 -0700336 rapt_token = get_rapt_token(
337 request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
338 )
339 body["rapt"] = rapt_token
340 (response_status_ok, response_data) = _client._token_endpoint_request_no_throw(
341 request, token_uri, body
342 )
343
344 if not response_status_ok:
345 _client._handle_error_response(response_data)
346 return _client._handle_refresh_grant_response(response_data, refresh_token) + (
347 rapt_token,
348 )