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