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. 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 37from six.moves import range 38 39from google.auth import exceptions 40from google.oauth2 import _client 41from google.oauth2 import _client_async 42from google.oauth2 import challenges 43from google.oauth2 import reauth 44 45 46async def _get_challenges( 47 request, supported_challenge_types, access_token, requested_scopes=None 48): 49 """Does initial request to reauth API to get the challenges. 50 51 Args: 52 request (google.auth.transport.Request): A callable used to make 53 HTTP requests. This must be an aiohttp request. 54 supported_challenge_types (Sequence[str]): list of challenge names 55 supported by the manager. 56 access_token (str): Access token with reauth scopes. 57 requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials. 58 59 Returns: 60 dict: The response from the reauth API. 61 """ 62 body = {"supportedChallengeTypes": supported_challenge_types} 63 if requested_scopes: 64 body["oauthScopesForDomainPolicyLookup"] = requested_scopes 65 66 return await _client_async._token_endpoint_request( 67 request, 68 reauth._REAUTH_API + ":start", 69 body, 70 access_token=access_token, 71 use_json=True, 72 ) 73 74 75async def _send_challenge_result( 76 request, session_id, challenge_id, client_input, access_token 77): 78 """Attempt to refresh access token by sending next challenge result. 79 80 Args: 81 request (google.auth.transport.Request): A callable used to make 82 HTTP requests. This must be an aiohttp request. 83 session_id (str): session id returned by the initial reauth call. 84 challenge_id (str): challenge id returned by the initial reauth call. 85 client_input: dict with a challenge-specific client input. For example: 86 ``{'credential': password}`` for password challenge. 87 access_token (str): Access token with reauth scopes. 88 89 Returns: 90 dict: The response from the reauth API. 91 """ 92 body = { 93 "sessionId": session_id, 94 "challengeId": challenge_id, 95 "action": "RESPOND", 96 "proposalResponse": client_input, 97 } 98 99 return await _client_async._token_endpoint_request( 100 request, 101 reauth._REAUTH_API + "/{}:continue".format(session_id), 102 body, 103 access_token=access_token, 104 use_json=True, 105 ) 106 107 108async def _run_next_challenge(msg, request, access_token): 109 """Get the next challenge from msg and run it. 110 111 Args: 112 msg (dict): Reauth API response body (either from the initial request to 113 https://reauth.googleapis.com/v2/sessions:start or from sending the 114 previous challenge response to 115 https://reauth.googleapis.com/v2/sessions/id:continue) 116 request (google.auth.transport.Request): A callable used to make 117 HTTP requests. This must be an aiohttp request. 118 access_token (str): reauth access token 119 120 Returns: 121 dict: The response from the reauth API. 122 123 Raises: 124 google.auth.exceptions.ReauthError: if reauth failed. 125 """ 126 for challenge in msg["challenges"]: 127 if challenge["status"] != "READY": 128 # Skip non-activated challenges. 129 continue 130 c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None) 131 if not c: 132 raise exceptions.ReauthFailError( 133 "Unsupported challenge type {0}. Supported types: {1}".format( 134 challenge["challengeType"], 135 ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())), 136 ) 137 ) 138 if not c.is_locally_eligible: 139 raise exceptions.ReauthFailError( 140 "Challenge {0} is not locally eligible".format( 141 challenge["challengeType"] 142 ) 143 ) 144 client_input = c.obtain_challenge_input(challenge) 145 if not client_input: 146 return None 147 return await _send_challenge_result( 148 request, 149 msg["sessionId"], 150 challenge["challengeId"], 151 client_input, 152 access_token, 153 ) 154 return None 155 156 157async def _obtain_rapt(request, access_token, requested_scopes): 158 """Given an http request method and reauth access token, get rapt token. 159 160 Args: 161 request (google.auth.transport.Request): A callable used to make 162 HTTP requests. This must be an aiohttp request. 163 access_token (str): reauth access token 164 requested_scopes (Sequence[str]): scopes required by the client application 165 166 Returns: 167 str: The rapt token. 168 169 Raises: 170 google.auth.exceptions.ReauthError: if reauth failed 171 """ 172 msg = await _get_challenges( 173 request, 174 list(challenges.AVAILABLE_CHALLENGES.keys()), 175 access_token, 176 requested_scopes, 177 ) 178 179 if msg["status"] == reauth._AUTHENTICATED: 180 return msg["encodedProofOfReauthToken"] 181 182 for _ in range(0, reauth.RUN_CHALLENGE_RETRY_LIMIT): 183 if not ( 184 msg["status"] == reauth._CHALLENGE_REQUIRED 185 or msg["status"] == reauth._CHALLENGE_PENDING 186 ): 187 raise exceptions.ReauthFailError( 188 "Reauthentication challenge failed due to API error: {}".format( 189 msg["status"] 190 ) 191 ) 192 193 if not reauth.is_interactive(): 194 raise exceptions.ReauthFailError( 195 "Reauthentication challenge could not be answered because you are not" 196 " in an interactive session." 197 ) 198 199 msg = await _run_next_challenge(msg, request, access_token) 200 201 if msg["status"] == reauth._AUTHENTICATED: 202 return msg["encodedProofOfReauthToken"] 203 204 # If we got here it means we didn't get authenticated. 205 raise exceptions.ReauthFailError("Failed to obtain rapt token.") 206 207 208async def get_rapt_token( 209 request, client_id, client_secret, refresh_token, token_uri, scopes=None 210): 211 """Given an http request method and refresh_token, get rapt token. 212 213 Args: 214 request (google.auth.transport.Request): A callable used to make 215 HTTP requests. This must be an aiohttp request. 216 client_id (str): client id to get access token for reauth scope. 217 client_secret (str): client secret for the client_id 218 refresh_token (str): refresh token to refresh access token 219 token_uri (str): uri to refresh access token 220 scopes (Optional(Sequence[str])): scopes required by the client application 221 222 Returns: 223 str: The rapt token. 224 Raises: 225 google.auth.exceptions.RefreshError: If reauth failed. 226 """ 227 sys.stderr.write("Reauthentication required.\n") 228 229 # Get access token for reauth. 230 access_token, _, _, _ = await _client_async.refresh_grant( 231 request=request, 232 client_id=client_id, 233 client_secret=client_secret, 234 refresh_token=refresh_token, 235 token_uri=token_uri, 236 scopes=[reauth._REAUTH_SCOPE], 237 ) 238 239 # Get rapt token from reauth API. 240 rapt_token = await _obtain_rapt(request, access_token, requested_scopes=scopes) 241 242 return rapt_token 243 244 245async def refresh_grant( 246 request, 247 token_uri, 248 refresh_token, 249 client_id, 250 client_secret, 251 scopes=None, 252 rapt_token=None, 253 enable_reauth_refresh=False, 254): 255 """Implements the reauthentication flow. 256 257 Args: 258 request (google.auth.transport.Request): A callable used to make 259 HTTP requests. This must be an aiohttp request. 260 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 261 URI. 262 refresh_token (str): The refresh token to use to get a new access 263 token. 264 client_id (str): The OAuth 2.0 application's client ID. 265 client_secret (str): The Oauth 2.0 appliaction's client secret. 266 scopes (Optional(Sequence[str])): Scopes to request. If present, all 267 scopes must be authorized for the refresh token. Useful if refresh 268 token has a wild card scope (e.g. 269 'https://www.googleapis.com/auth/any-api'). 270 rapt_token (Optional(str)): The rapt token for reauth. 271 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow 272 should be used. The default value is False. This option is for 273 gcloud only, other users should use the default value. 274 275 Returns: 276 Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The 277 access token, new refresh token, expiration, the additional data 278 returned by the token endpoint, and the rapt token. 279 280 Raises: 281 google.auth.exceptions.RefreshError: If the token endpoint returned 282 an error. 283 """ 284 body = { 285 "grant_type": _client._REFRESH_GRANT_TYPE, 286 "client_id": client_id, 287 "client_secret": client_secret, 288 "refresh_token": refresh_token, 289 } 290 if scopes: 291 body["scope"] = " ".join(scopes) 292 if rapt_token: 293 body["rapt"] = rapt_token 294 295 response_status_ok, response_data = await _client_async._token_endpoint_request_no_throw( 296 request, token_uri, body 297 ) 298 if ( 299 not response_status_ok 300 and response_data.get("error") == reauth._REAUTH_NEEDED_ERROR 301 and ( 302 response_data.get("error_subtype") 303 == reauth._REAUTH_NEEDED_ERROR_INVALID_RAPT 304 or response_data.get("error_subtype") 305 == reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED 306 ) 307 ): 308 if not enable_reauth_refresh: 309 raise exceptions.RefreshError( 310 "Reauthentication is needed. Please run `gcloud auth login --update-adc` to reauthenticate." 311 ) 312 313 rapt_token = await get_rapt_token( 314 request, client_id, client_secret, refresh_token, token_uri, scopes=scopes 315 ) 316 body["rapt"] = rapt_token 317 ( 318 response_status_ok, 319 response_data, 320 ) = await _client_async._token_endpoint_request_no_throw( 321 request, token_uri, body 322 ) 323 324 if not response_status_ok: 325 _client._handle_error_response(response_data) 326 refresh_response = _client._handle_refresh_grant_response( 327 response_data, refresh_token 328 ) 329 return refresh_response + (rapt_token,) 330