Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/oauth2/reauth.py: 28%
80 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# 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.
15"""A module that provides functions for handling rapt authentication.
17Reauth is a process of obtaining additional authentication (such as password,
18security token, etc.) while refreshing OAuth 2.0 credentials for a user.
20Credentials that use the Reauth flow must have the reauth scope,
21``https://www.googleapis.com/auth/accounts.reauth``.
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.
27Those steps are:
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"""
35import sys
37from google.auth import exceptions
38from google.auth import metrics
39from google.oauth2 import _client
40from google.oauth2 import challenges
43_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"
44_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"
46_REAUTH_NEEDED_ERROR = "invalid_grant"
47_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"
48_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"
50_AUTHENTICATED = "AUTHENTICATED"
51_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"
52_CHALLENGE_PENDING = "CHALLENGE_PENDING"
55# Override this global variable to set custom max number of rounds of reauth
56# challenges should be run.
57RUN_CHALLENGE_RETRY_LIMIT = 5
60def is_interactive():
61 """Check if we are in an interractive environment.
63 Override this function with a different logic if you are using this library
64 outside a CLI.
66 If the rapt token needs refreshing, the user needs to answer the challenges.
67 If the user is not in an interractive environment, the challenges can not
68 be answered and we just wait for timeout for no reason.
70 Returns:
71 bool: True if is interactive environment, False otherwise.
72 """
74 return sys.stdin.isatty()
77def _get_challenges(
78 request, supported_challenge_types, access_token, requested_scopes=None
79):
80 """Does initial request to reauth API to get the challenges.
82 Args:
83 request (google.auth.transport.Request): A callable used to make
84 HTTP requests.
85 supported_challenge_types (Sequence[str]): list of challenge names
86 supported by the manager.
87 access_token (str): Access token with reauth scopes.
88 requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
90 Returns:
91 dict: The response from the reauth API.
92 """
93 body = {"supportedChallengeTypes": supported_challenge_types}
94 if requested_scopes:
95 body["oauthScopesForDomainPolicyLookup"] = requested_scopes
96 metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_start()}
98 return _client._token_endpoint_request(
99 request,
100 _REAUTH_API + ":start",
101 body,
102 access_token=access_token,
103 use_json=True,
104 headers=metrics_header,
105 )
108def _send_challenge_result(
109 request, session_id, challenge_id, client_input, access_token
110):
111 """Attempt to refresh access token by sending next challenge result.
113 Args:
114 request (google.auth.transport.Request): A callable used to make
115 HTTP requests.
116 session_id (str): session id returned by the initial reauth call.
117 challenge_id (str): challenge id returned by the initial reauth call.
118 client_input: dict with a challenge-specific client input. For example:
119 ``{'credential': password}`` for password challenge.
120 access_token (str): Access token with reauth scopes.
122 Returns:
123 dict: The response from the reauth API.
124 """
125 body = {
126 "sessionId": session_id,
127 "challengeId": challenge_id,
128 "action": "RESPOND",
129 "proposalResponse": client_input,
130 }
131 metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_continue()}
133 return _client._token_endpoint_request(
134 request,
135 _REAUTH_API + "/{}:continue".format(session_id),
136 body,
137 access_token=access_token,
138 use_json=True,
139 headers=metrics_header,
140 )
143def _run_next_challenge(msg, request, access_token):
144 """Get the next challenge from msg and run it.
146 Args:
147 msg (dict): Reauth API response body (either from the initial request to
148 https://reauth.googleapis.com/v2/sessions:start or from sending the
149 previous challenge response to
150 https://reauth.googleapis.com/v2/sessions/id:continue)
151 request (google.auth.transport.Request): A callable used to make
152 HTTP requests.
153 access_token (str): reauth access token
155 Returns:
156 dict: The response from the reauth API.
158 Raises:
159 google.auth.exceptions.ReauthError: if reauth failed.
160 """
161 for challenge in msg["challenges"]:
162 if challenge["status"] != "READY":
163 # Skip non-activated challenges.
164 continue
165 c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
166 if not c:
167 raise exceptions.ReauthFailError(
168 "Unsupported challenge type {0}. Supported types: {1}".format(
169 challenge["challengeType"],
170 ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
171 )
172 )
173 if not c.is_locally_eligible:
174 raise exceptions.ReauthFailError(
175 "Challenge {0} is not locally eligible".format(
176 challenge["challengeType"]
177 )
178 )
179 client_input = c.obtain_challenge_input(challenge)
180 if not client_input:
181 return None
182 return _send_challenge_result(
183 request,
184 msg["sessionId"],
185 challenge["challengeId"],
186 client_input,
187 access_token,
188 )
189 return None
192def _obtain_rapt(request, access_token, requested_scopes):
193 """Given an http request method and reauth access token, get rapt token.
195 Args:
196 request (google.auth.transport.Request): A callable used to make
197 HTTP requests.
198 access_token (str): reauth access token
199 requested_scopes (Sequence[str]): scopes required by the client application
201 Returns:
202 str: The rapt token.
204 Raises:
205 google.auth.exceptions.ReauthError: if reauth failed
206 """
207 msg = _get_challenges(
208 request,
209 list(challenges.AVAILABLE_CHALLENGES.keys()),
210 access_token,
211 requested_scopes,
212 )
214 if msg["status"] == _AUTHENTICATED:
215 return msg["encodedProofOfReauthToken"]
217 for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):
218 if not (
219 msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING
220 ):
221 raise exceptions.ReauthFailError(
222 "Reauthentication challenge failed due to API error: {}".format(
223 msg["status"]
224 )
225 )
227 if not is_interactive():
228 raise exceptions.ReauthFailError(
229 "Reauthentication challenge could not be answered because you are not"
230 " in an interactive session."
231 )
233 msg = _run_next_challenge(msg, request, access_token)
235 if not msg:
236 raise exceptions.ReauthFailError("Failed to obtain rapt token.")
237 if msg["status"] == _AUTHENTICATED:
238 return msg["encodedProofOfReauthToken"]
240 # If we got here it means we didn't get authenticated.
241 raise exceptions.ReauthFailError("Failed to obtain rapt token.")
244def get_rapt_token(
245 request, client_id, client_secret, refresh_token, token_uri, scopes=None
246):
247 """Given an http request method and refresh_token, get rapt token.
249 Args:
250 request (google.auth.transport.Request): A callable used to make
251 HTTP requests.
252 client_id (str): client id to get access token for reauth scope.
253 client_secret (str): client secret for the client_id
254 refresh_token (str): refresh token to refresh access token
255 token_uri (str): uri to refresh access token
256 scopes (Optional(Sequence[str])): scopes required by the client application
258 Returns:
259 str: The rapt token.
260 Raises:
261 google.auth.exceptions.RefreshError: If reauth failed.
262 """
263 sys.stderr.write("Reauthentication required.\n")
265 # Get access token for reauth.
266 access_token, _, _, _ = _client.refresh_grant(
267 request=request,
268 client_id=client_id,
269 client_secret=client_secret,
270 refresh_token=refresh_token,
271 token_uri=token_uri,
272 scopes=[_REAUTH_SCOPE],
273 )
275 # Get rapt token from reauth API.
276 rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
278 return rapt_token
281def refresh_grant(
282 request,
283 token_uri,
284 refresh_token,
285 client_id,
286 client_secret,
287 scopes=None,
288 rapt_token=None,
289 enable_reauth_refresh=False,
290):
291 """Implements the reauthentication flow.
293 Args:
294 request (google.auth.transport.Request): A callable used to make
295 HTTP requests.
296 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
297 URI.
298 refresh_token (str): The refresh token to use to get a new access
299 token.
300 client_id (str): The OAuth 2.0 application's client ID.
301 client_secret (str): The Oauth 2.0 appliaction's client secret.
302 scopes (Optional(Sequence[str])): Scopes to request. If present, all
303 scopes must be authorized for the refresh token. Useful if refresh
304 token has a wild card scope (e.g.
305 'https://www.googleapis.com/auth/any-api').
306 rapt_token (Optional(str)): The rapt token for reauth.
307 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
308 should be used. The default value is False. This option is for
309 gcloud only, other users should use the default value.
311 Returns:
312 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
313 access token, new refresh token, expiration, the additional data
314 returned by the token endpoint, and the rapt token.
316 Raises:
317 google.auth.exceptions.RefreshError: If the token endpoint returned
318 an error.
319 """
320 body = {
321 "grant_type": _client._REFRESH_GRANT_TYPE,
322 "client_id": client_id,
323 "client_secret": client_secret,
324 "refresh_token": refresh_token,
325 }
326 if scopes:
327 body["scope"] = " ".join(scopes)
328 if rapt_token:
329 body["rapt"] = rapt_token
330 metrics_header = {metrics.API_CLIENT_HEADER: metrics.token_request_user()}
332 response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw(
333 request, token_uri, body, headers=metrics_header
334 )
336 if not response_status_ok and isinstance(response_data, str):
337 raise exceptions.RefreshError(response_data, retryable=False)
339 if (
340 not response_status_ok
341 and response_data.get("error") == _REAUTH_NEEDED_ERROR
342 and (
343 response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
344 or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
345 )
346 ):
347 if not enable_reauth_refresh:
348 raise exceptions.RefreshError(
349 "Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
350 )
352 rapt_token = get_rapt_token(
353 request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
354 )
355 body["rapt"] = rapt_token
356 (
357 response_status_ok,
358 response_data,
359 retryable_error,
360 ) = _client._token_endpoint_request_no_throw(
361 request, token_uri, body, headers=metrics_header
362 )
364 if not response_status_ok:
365 _client._handle_error_response(response_data, retryable_error)
366 return _client._handle_refresh_grant_response(response_data, refresh_token) + (
367 rapt_token,
368 )