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

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 google.auth import exceptions 

38from google.auth import metrics 

39from google.oauth2 import _client 

40from google.oauth2 import challenges 

41 

42 

43_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth" 

44_REAUTH_API = "https://reauth.googleapis.com/v2/sessions" 

45 

46_REAUTH_NEEDED_ERROR = "invalid_grant" 

47_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt" 

48_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required" 

49 

50_AUTHENTICATED = "AUTHENTICATED" 

51_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED" 

52_CHALLENGE_PENDING = "CHALLENGE_PENDING" 

53 

54 

55# Override this global variable to set custom max number of rounds of reauth 

56# challenges should be run. 

57RUN_CHALLENGE_RETRY_LIMIT = 5 

58 

59 

60def is_interactive(): 

61 """Check if we are in an interractive environment. 

62 

63 Override this function with a different logic if you are using this library 

64 outside a CLI. 

65 

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. 

69 

70 Returns: 

71 bool: True if is interactive environment, False otherwise. 

72 """ 

73 

74 return sys.stdin.isatty() 

75 

76 

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. 

81 

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. 

89 

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()} 

97 

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 ) 

106 

107 

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. 

112 

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. 

121 

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()} 

132 

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 ) 

141 

142 

143def _run_next_challenge(msg, request, access_token): 

144 """Get the next challenge from msg and run it. 

145 

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 

154 

155 Returns: 

156 dict: The response from the reauth API. 

157 

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 

190 

191 

192def _obtain_rapt(request, access_token, requested_scopes): 

193 """Given an http request method and reauth access token, get rapt token. 

194 

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 

200 

201 Returns: 

202 str: The rapt token. 

203 

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 ) 

213 

214 if msg["status"] == _AUTHENTICATED: 

215 return msg["encodedProofOfReauthToken"] 

216 

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 ) 

226 

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 ) 

232 

233 msg = _run_next_challenge(msg, request, access_token) 

234 

235 if not msg: 

236 raise exceptions.ReauthFailError("Failed to obtain rapt token.") 

237 if msg["status"] == _AUTHENTICATED: 

238 return msg["encodedProofOfReauthToken"] 

239 

240 # If we got here it means we didn't get authenticated. 

241 raise exceptions.ReauthFailError("Failed to obtain rapt token.") 

242 

243 

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. 

248 

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 

257 

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") 

264 

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 ) 

274 

275 # Get rapt token from reauth API. 

276 rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes) 

277 

278 return rapt_token 

279 

280 

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. 

292 

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. 

310 

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. 

315 

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()} 

331 

332 response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw( 

333 request, token_uri, body, headers=metrics_header 

334 ) 

335 

336 if not response_status_ok and isinstance(response_data, str): 

337 raise exceptions.RefreshError(response_data, retryable=False) 

338 

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 ) 

351 

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 ) 

363 

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 )