Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google_auth_oauthlib/flow.py: 38%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

111 statements  

1# Copyright 2016 Google Inc. 

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"""OAuth 2.0 Authorization Flow 

16 

17This module provides integration with `requests-oauthlib`_ for running the 

18`OAuth 2.0 Authorization Flow`_ and acquiring user credentials. See 

19`Using OAuth 2.0 to Access Google APIs`_ for an overview of OAuth 2.0 

20authorization scenarios Google APIs support. 

21 

22Here's an example of using :class:`InstalledAppFlow`:: 

23 

24 from google_auth_oauthlib.flow import InstalledAppFlow 

25 

26 # Create the flow using the client secrets file from the Google API 

27 # Console. 

28 flow = InstalledAppFlow.from_client_secrets_file( 

29 'client_secrets.json', 

30 scopes=['profile', 'email']) 

31 

32 flow.run_local_server() 

33 

34 # You can use flow.credentials, or you can just get a requests session 

35 # using flow.authorized_session. 

36 session = flow.authorized_session() 

37 

38 profile_info = session.get( 

39 'https://www.googleapis.com/userinfo/v2/me').json() 

40 

41 print(profile_info) 

42 # {'name': '...', 'email': '...', ...} 

43 

44.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/latest/ 

45.. _OAuth 2.0 Authorization Flow: 

46 https://tools.ietf.org/html/rfc6749#section-1.2 

47.. _Using OAuth 2.0 to Access Google APIs: 

48 https://developers.google.com/identity/protocols/oauth2 

49 

50""" 

51from base64 import urlsafe_b64encode 

52import hashlib 

53import json 

54import logging 

55 

56try: 

57 from secrets import SystemRandom 

58except ImportError: # pragma: NO COVER 

59 from random import SystemRandom 

60from string import ascii_letters, digits 

61import webbrowser 

62import wsgiref.simple_server 

63import wsgiref.util 

64 

65import google.auth.transport.requests 

66import google.oauth2.credentials 

67 

68import google_auth_oauthlib.helpers 

69 

70 

71_LOGGER = logging.getLogger(__name__) 

72 

73 

74class Flow(object): 

75 """OAuth 2.0 Authorization Flow 

76 

77 This class uses a :class:`requests_oauthlib.OAuth2Session` instance at 

78 :attr:`oauth2session` to perform all of the OAuth 2.0 logic. This class 

79 just provides convenience methods and sane defaults for doing Google's 

80 particular flavors of OAuth 2.0. 

81 

82 Typically you'll construct an instance of this flow using 

83 :meth:`from_client_secrets_file` and a `client secrets file`_ obtained 

84 from the `Google API Console`_. 

85 

86 .. _client secrets file: 

87 https://developers.google.com/identity/protocols/oauth2/web-server 

88 #creatingcred 

89 .. _Google API Console: 

90 https://console.developers.google.com/apis/credentials 

91 """ 

92 

93 def __init__( 

94 self, 

95 oauth2session, 

96 client_type, 

97 client_config, 

98 redirect_uri=None, 

99 code_verifier=None, 

100 autogenerate_code_verifier=True, 

101 ): 

102 """ 

103 Args: 

104 oauth2session (requests_oauthlib.OAuth2Session): 

105 The OAuth 2.0 session from ``requests-oauthlib``. 

106 client_type (str): The client type, either ``web`` or 

107 ``installed``. 

108 client_config (Mapping[str, Any]): The client 

109 configuration in the Google `client secrets`_ format. 

110 redirect_uri (str): The OAuth 2.0 redirect URI if known at flow 

111 creation time. Otherwise, it will need to be set using 

112 :attr:`redirect_uri`. 

113 code_verifier (str): random string of 43-128 chars used to verify 

114 the key exchange.using PKCE. 

115 autogenerate_code_verifier (bool): If true, auto-generate a 

116 code_verifier. 

117 .. _client secrets: 

118 https://github.com/googleapis/google-api-python-client/blob 

119 /main/docs/client-secrets.md 

120 """ 

121 self.client_type = client_type 

122 """str: The client type, either ``'web'`` or ``'installed'``""" 

123 self.client_config = client_config[client_type] 

124 """Mapping[str, Any]: The OAuth 2.0 client configuration.""" 

125 self.oauth2session = oauth2session 

126 """requests_oauthlib.OAuth2Session: The OAuth 2.0 session.""" 

127 self.redirect_uri = redirect_uri 

128 self.code_verifier = code_verifier 

129 self.autogenerate_code_verifier = autogenerate_code_verifier 

130 

131 @classmethod 

132 def from_client_config(cls, client_config, scopes, **kwargs): 

133 """Creates a :class:`requests_oauthlib.OAuth2Session` from client 

134 configuration loaded from a Google-format client secrets file. 

135 

136 Args: 

137 client_config (Mapping[str, Any]): The client 

138 configuration in the Google `client secrets`_ format. 

139 scopes (Sequence[str]): The list of scopes to request during the 

140 flow. 

141 kwargs: Any additional parameters passed to 

142 :class:`requests_oauthlib.OAuth2Session` 

143 

144 Returns: 

145 Flow: The constructed Flow instance. 

146 

147 Raises: 

148 ValueError: If the client configuration is not in the correct 

149 format. 

150 

151 .. _client secrets: 

152 https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md 

153 """ 

154 if "web" in client_config: 

155 client_type = "web" 

156 elif "installed" in client_config: 

157 client_type = "installed" 

158 else: 

159 raise ValueError("Client secrets must be for a web or installed app.") 

160 

161 # these args cannot be passed to requests_oauthlib.OAuth2Session 

162 code_verifier = kwargs.pop("code_verifier", None) 

163 autogenerate_code_verifier = kwargs.pop("autogenerate_code_verifier", None) 

164 

165 ( 

166 session, 

167 client_config, 

168 ) = google_auth_oauthlib.helpers.session_from_client_config( 

169 client_config, scopes, **kwargs 

170 ) 

171 

172 redirect_uri = kwargs.get("redirect_uri", None) 

173 

174 return cls( 

175 session, 

176 client_type, 

177 client_config, 

178 redirect_uri, 

179 code_verifier, 

180 autogenerate_code_verifier, 

181 ) 

182 

183 @classmethod 

184 def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs): 

185 """Creates a :class:`Flow` instance from a Google client secrets file. 

186 

187 Args: 

188 client_secrets_file (str): The path to the client secrets .json 

189 file. 

190 scopes (Sequence[str]): The list of scopes to request during the 

191 flow. 

192 kwargs: Any additional parameters passed to 

193 :class:`requests_oauthlib.OAuth2Session` 

194 

195 Returns: 

196 Flow: The constructed Flow instance. 

197 """ 

198 with open(client_secrets_file, "r") as json_file: 

199 client_config = json.load(json_file) 

200 

201 return cls.from_client_config(client_config, scopes=scopes, **kwargs) 

202 

203 @property 

204 def redirect_uri(self): 

205 """The OAuth 2.0 redirect URI. Pass-through to 

206 ``self.oauth2session.redirect_uri``.""" 

207 return self.oauth2session.redirect_uri 

208 

209 @redirect_uri.setter 

210 def redirect_uri(self, value): 

211 """The OAuth 2.0 redirect URI. Pass-through to 

212 ``self.oauth2session.redirect_uri``.""" 

213 self.oauth2session.redirect_uri = value 

214 

215 def authorization_url(self, **kwargs): 

216 """Generates an authorization URL. 

217 

218 This is the first step in the OAuth 2.0 Authorization Flow. The user's 

219 browser should be redirected to the returned URL. 

220 

221 This method calls 

222 :meth:`requests_oauthlib.OAuth2Session.authorization_url` 

223 and specifies the client configuration's authorization URI (usually 

224 Google's authorization server) and specifies that "offline" access is 

225 desired. This is required in order to obtain a refresh token. 

226 

227 Args: 

228 kwargs: Additional arguments passed through to 

229 :meth:`requests_oauthlib.OAuth2Session.authorization_url` 

230 

231 Returns: 

232 Tuple[str, str]: The generated authorization URL and state. The 

233 user must visit the URL to complete the flow. The state is used 

234 when completing the flow to verify that the request originated 

235 from your application. If your application is using a different 

236 :class:`Flow` instance to obtain the token, you will need to 

237 specify the ``state`` when constructing the :class:`Flow`. 

238 """ 

239 kwargs.setdefault("access_type", "offline") 

240 if self.autogenerate_code_verifier: 

241 chars = ascii_letters + digits + "-._~" 

242 rnd = SystemRandom() 

243 random_verifier = [rnd.choice(chars) for _ in range(0, 128)] 

244 self.code_verifier = "".join(random_verifier) 

245 

246 if self.code_verifier: 

247 code_hash = hashlib.sha256() 

248 code_hash.update(str.encode(self.code_verifier)) 

249 unencoded_challenge = code_hash.digest() 

250 b64_challenge = urlsafe_b64encode(unencoded_challenge) 

251 code_challenge = b64_challenge.decode().split("=")[0] 

252 kwargs.setdefault("code_challenge", code_challenge) 

253 kwargs.setdefault("code_challenge_method", "S256") 

254 url, state = self.oauth2session.authorization_url( 

255 self.client_config["auth_uri"], **kwargs 

256 ) 

257 

258 return url, state 

259 

260 def fetch_token(self, **kwargs): 

261 """Completes the Authorization Flow and obtains an access token. 

262 

263 This is the final step in the OAuth 2.0 Authorization Flow. This is 

264 called after the user consents. 

265 

266 This method calls 

267 :meth:`requests_oauthlib.OAuth2Session.fetch_token` 

268 and specifies the client configuration's token URI (usually Google's 

269 token server). 

270 

271 Args: 

272 kwargs: Arguments passed through to 

273 :meth:`requests_oauthlib.OAuth2Session.fetch_token`. At least 

274 one of ``code`` or ``authorization_response`` must be 

275 specified. 

276 

277 Returns: 

278 Mapping[str, str]: The obtained tokens. Typically, you will not use 

279 return value of this function and instead use 

280 :meth:`credentials` to obtain a 

281 :class:`~google.auth.credentials.Credentials` instance. 

282 """ 

283 kwargs.setdefault("client_secret", self.client_config["client_secret"]) 

284 kwargs.setdefault("code_verifier", self.code_verifier) 

285 return self.oauth2session.fetch_token(self.client_config["token_uri"], **kwargs) 

286 

287 @property 

288 def credentials(self): 

289 """Returns credentials from the OAuth 2.0 session. 

290 

291 :meth:`fetch_token` must be called before accessing this. This method 

292 constructs a :class:`google.oauth2.credentials.Credentials` class using 

293 the session's token and the client config. 

294 

295 Returns: 

296 google.oauth2.credentials.Credentials: The constructed credentials. 

297 

298 Raises: 

299 ValueError: If there is no access token in the session. 

300 """ 

301 return google_auth_oauthlib.helpers.credentials_from_session( 

302 self.oauth2session, self.client_config 

303 ) 

304 

305 def authorized_session(self): 

306 """Returns a :class:`requests.Session` authorized with credentials. 

307 

308 :meth:`fetch_token` must be called before this method. This method 

309 constructs a :class:`google.auth.transport.requests.AuthorizedSession` 

310 class using this flow's :attr:`credentials`. 

311 

312 Returns: 

313 google.auth.transport.requests.AuthorizedSession: The constructed 

314 session. 

315 """ 

316 return google.auth.transport.requests.AuthorizedSession(self.credentials) 

317 

318 

319class InstalledAppFlow(Flow): 

320 """Authorization flow helper for installed applications. 

321 

322 This :class:`Flow` subclass makes it easier to perform the 

323 `Installed Application Authorization Flow`_. This flow is useful for 

324 local development or applications that are installed on a desktop operating 

325 system. 

326 

327 This flow uses a local server strategy provided by :meth:`run_local_server`. 

328 

329 Example:: 

330 

331 from google_auth_oauthlib.flow import InstalledAppFlow 

332 

333 flow = InstalledAppFlow.from_client_secrets_file( 

334 'client_secrets.json', 

335 scopes=['profile', 'email']) 

336 

337 flow.run_local_server() 

338 

339 session = flow.authorized_session() 

340 

341 profile_info = session.get( 

342 'https://www.googleapis.com/userinfo/v2/me').json() 

343 

344 print(profile_info) 

345 # {'name': '...', 'email': '...', ...} 

346 

347 

348 Note that this isn't the only way to accomplish the installed 

349 application flow, just one of the most common. You can use the 

350 :class:`Flow` class to perform the same flow with different methods of 

351 presenting the authorization URL to the user or obtaining the authorization 

352 response, such as using an embedded web view. 

353 

354 .. _Installed Application Authorization Flow: 

355 https://github.com/googleapis/google-api-python-client/blob/main/docs/oauth-installed.md 

356 """ 

357 

358 _DEFAULT_AUTH_PROMPT_MESSAGE = ( 

359 "Please visit this URL to authorize this application: {url}" 

360 ) 

361 """str: The message to display when prompting the user for 

362 authorization.""" 

363 _DEFAULT_AUTH_CODE_MESSAGE = "Enter the authorization code: " 

364 """str: The message to display when prompting the user for the 

365 authorization code. Used only by the console strategy.""" 

366 

367 _DEFAULT_WEB_SUCCESS_MESSAGE = ( 

368 "The authentication flow has completed. You may close this window." 

369 ) 

370 

371 def run_local_server( 

372 self, 

373 host="localhost", 

374 bind_addr=None, 

375 port=8080, 

376 authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, 

377 success_message=_DEFAULT_WEB_SUCCESS_MESSAGE, 

378 open_browser=True, 

379 redirect_uri_trailing_slash=True, 

380 timeout_seconds=None, 

381 token_audience=None, 

382 browser=None, 

383 **kwargs 

384 ): 

385 """Run the flow using the server strategy. 

386 

387 The server strategy instructs the user to open the authorization URL in 

388 their browser and will attempt to automatically open the URL for them. 

389 It will start a local web server to listen for the authorization 

390 response. Once authorization is complete the authorization server will 

391 redirect the user's browser to the local web server. The web server 

392 will get the authorization code from the response and shutdown. The 

393 code is then exchanged for a token. 

394 

395 Args: 

396 host (str): The hostname for the local redirect server. This will 

397 be served over http, not https. 

398 bind_addr (str): Optionally provide an ip address for the redirect 

399 server to listen on when it is not the same as host 

400 (e.g. in a container). Default value is None, 

401 which means that the redirect server will listen 

402 on the ip address specified in the host parameter. 

403 port (int): The port for the local redirect server. 

404 authorization_prompt_message (str | None): The message to display to tell 

405 the user to navigate to the authorization URL. If None or empty, 

406 don't display anything. 

407 success_message (str): The message to display in the web browser 

408 the authorization flow is complete. 

409 open_browser (bool): Whether or not to open the authorization URL 

410 in the user's browser. 

411 redirect_uri_trailing_slash (bool): whether or not to add trailing 

412 slash when constructing the redirect_uri. Default value is True. 

413 timeout_seconds (int): It will raise an error after the timeout timing 

414 if there are no credentials response. The value is in seconds. 

415 When set to None there is no timeout. 

416 Default value is None. 

417 token_audience (str): Passed along with the request for an access 

418 token. Determines the endpoints with which the token can be 

419 used. Optional. 

420 browser (str): specify which browser to open for authentication. If not 

421 specified this defaults to default browser. 

422 kwargs: Additional keyword arguments passed through to 

423 :meth:`authorization_url`. 

424 

425 Returns: 

426 google.oauth2.credentials.Credentials: The OAuth 2.0 credentials 

427 for the user. 

428 """ 

429 wsgi_app = _RedirectWSGIApp(success_message) 

430 # Fail fast if the address is occupied 

431 wsgiref.simple_server.WSGIServer.allow_reuse_address = False 

432 local_server = wsgiref.simple_server.make_server( 

433 bind_addr or host, port, wsgi_app, handler_class=_WSGIRequestHandler 

434 ) 

435 

436 try: 

437 redirect_uri_format = ( 

438 "http://{}:{}/" if redirect_uri_trailing_slash else "http://{}:{}" 

439 ) 

440 self.redirect_uri = redirect_uri_format.format( 

441 host, local_server.server_port 

442 ) 

443 auth_url, _ = self.authorization_url(**kwargs) 

444 

445 if open_browser: 

446 # if browser is None it defaults to default browser 

447 webbrowser.get(browser).open(auth_url, new=1, autoraise=True) 

448 

449 if authorization_prompt_message: 

450 print(authorization_prompt_message.format(url=auth_url)) 

451 

452 local_server.timeout = timeout_seconds 

453 local_server.handle_request() 

454 

455 # Note: using https here because oauthlib is very picky that 

456 # OAuth 2.0 should only occur over https. 

457 authorization_response = wsgi_app.last_request_uri.replace("http", "https") 

458 self.fetch_token( 

459 authorization_response=authorization_response, audience=token_audience 

460 ) 

461 finally: 

462 local_server.server_close() 

463 

464 return self.credentials 

465 

466 

467class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler): 

468 """Custom WSGIRequestHandler. 

469 

470 Uses a named logger instead of printing to stderr. 

471 """ 

472 

473 def log_message(self, format, *args): 

474 # pylint: disable=redefined-builtin 

475 # (format is the argument name defined in the superclass.) 

476 _LOGGER.info(format, *args) 

477 

478 

479class _RedirectWSGIApp(object): 

480 """WSGI app to handle the authorization redirect. 

481 

482 Stores the request URI and displays the given success message. 

483 """ 

484 

485 def __init__(self, success_message): 

486 """ 

487 Args: 

488 success_message (str): The message to display in the web browser 

489 the authorization flow is complete. 

490 """ 

491 self.last_request_uri = None 

492 self._success_message = success_message 

493 

494 def __call__(self, environ, start_response): 

495 """WSGI Callable. 

496 

497 Args: 

498 environ (Mapping[str, Any]): The WSGI environment. 

499 start_response (Callable[str, list]): The WSGI start_response 

500 callable. 

501 

502 Returns: 

503 Iterable[bytes]: The response body. 

504 """ 

505 start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")]) 

506 self.last_request_uri = wsgiref.util.request_uri(environ) 

507 return [self._success_message.encode("utf-8")]