blob: fc2629e828c448aa113e5454aa0ce5ce1062f46c [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,
278):
279 """Implements the reauthentication flow.
280
281 Args:
282 request (google.auth.transport.Request): A callable used to make
283 HTTP requests.
284 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
285 URI.
286 refresh_token (str): The refresh token to use to get a new access
287 token.
288 client_id (str): The OAuth 2.0 application's client ID.
289 client_secret (str): The Oauth 2.0 appliaction's client secret.
290 scopes (Optional(Sequence[str])): Scopes to request. If present, all
291 scopes must be authorized for the refresh token. Useful if refresh
292 token has a wild card scope (e.g.
293 'https://www.googleapis.com/auth/any-api').
294 rapt_token (Optional(str)): The rapt token for reauth.
295
296 Returns:
arithmetic17289e108232021-04-23 15:27:02 -0700297 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
298 access token, new refresh token, expiration, the additional data
299 returned by the token endpoint, and the rapt token.
arithmetic172882293fe2021-04-14 11:22:13 -0700300
301 Raises:
302 google.auth.exceptions.RefreshError: If the token endpoint returned
303 an error.
304 """
305 body = {
306 "grant_type": _client._REFRESH_GRANT_TYPE,
307 "client_id": client_id,
308 "client_secret": client_secret,
309 "refresh_token": refresh_token,
310 }
311 if scopes:
312 body["scope"] = " ".join(scopes)
313 if rapt_token:
314 body["rapt"] = rapt_token
315
316 response_status_ok, response_data = _client._token_endpoint_request_no_throw(
317 request, token_uri, body
318 )
319 if (
320 not response_status_ok
321 and response_data.get("error") == _REAUTH_NEEDED_ERROR
322 and (
323 response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
324 or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
325 )
326 ):
327 rapt_token = get_rapt_token(
328 request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
329 )
330 body["rapt"] = rapt_token
331 (response_status_ok, response_data) = _client._token_endpoint_request_no_throw(
332 request, token_uri, body
333 )
334
335 if not response_status_ok:
336 _client._handle_error_response(response_data)
337 return _client._handle_refresh_grant_response(response_data, refresh_token) + (
338 rapt_token,
339 )