|
17 | 17 | This module provides integration with `requests-oauthlib`_ for running the |
18 | 18 | `OAuth 2.0 Authorization Flow`_ and acquiring user credentials. |
19 | 19 |
|
20 | | -Here's an example of using the flow with the installed application |
| 20 | +Here's an example of using :class:`Flow` with the installed application |
21 | 21 | authorization flow:: |
22 | 22 |
|
23 | 23 | from google_auth_oauthlib.flow import Flow |
|
44 | 44 | session = flow.authorized_session() |
45 | 45 | print(session.get('https://www.googleapis.com/userinfo/v2/me').json()) |
46 | 46 |
|
| 47 | +This particular flow can be handled entirely by using |
| 48 | +:class:`InstalledAppFlow`. |
| 49 | +
|
47 | 50 | .. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/ |
48 | 51 | .. _OAuth 2.0 Authorization Flow: |
49 | 52 | https://tools.ietf.org/html/rfc6749#section-1.2 |
50 | 53 | """ |
51 | 54 |
|
52 | 55 | import json |
| 56 | +import logging |
| 57 | +import webbrowser |
| 58 | +import wsgiref.simple_server |
| 59 | +import wsgiref.util |
53 | 60 |
|
54 | 61 | import google.auth.transport.requests |
55 | 62 | import google.oauth2.credentials |
| 63 | +from six.moves import input |
56 | 64 |
|
57 | 65 | import google_auth_oauthlib.helpers |
58 | 66 |
|
59 | 67 |
|
| 68 | +_LOGGER = logging.getLogger(__name__) |
| 69 | + |
| 70 | + |
60 | 71 | class Flow(object): |
61 | 72 | """OAuth 2.0 Authorization Flow |
62 | 73 |
|
@@ -253,3 +264,195 @@ class using this flow's :attr:`credentials`. |
253 | 264 | """ |
254 | 265 | return google.auth.transport.requests.AuthorizedSession( |
255 | 266 | self.credentials) |
| 267 | + |
| 268 | + |
| 269 | +class InstalledAppFlow(Flow): |
| 270 | + """Authorization flow helper for installed applications. |
| 271 | +
|
| 272 | + This :class:`Flow` subclass makes it easier to perform the |
| 273 | + `Installed Application Authorization Flow`_. This flow is useful for |
| 274 | + local development or applications that are installed on a desktop operating |
| 275 | + system. |
| 276 | +
|
| 277 | + This flow has two strategies: The console strategy provided by |
| 278 | + :meth:`run_console` and the local server strategy provided by |
| 279 | + :meth:`run_local_server`. |
| 280 | +
|
| 281 | + Example:: |
| 282 | +
|
| 283 | + from google_auth_oauthlib.flow import InstalledAppFlow |
| 284 | +
|
| 285 | + flow = InstalledAppFlow.from_client_secrets_file( |
| 286 | + 'client_secrets.json', |
| 287 | + scopes=['profile', 'email']) |
| 288 | +
|
| 289 | + flow.run_local_server() |
| 290 | +
|
| 291 | + session = flow.authorized_session() |
| 292 | +
|
| 293 | + profile_info = session.get( |
| 294 | + 'https://www.googleapis.com/userinfo/v2/me').json() |
| 295 | +
|
| 296 | + print(profile_info) |
| 297 | + # {'name': '...', 'email': '...', ...} |
| 298 | +
|
| 299 | +
|
| 300 | + Note that these aren't the only two ways to accomplish the installed |
| 301 | + application flow, they are just the most common ways. You can use the |
| 302 | + :class:`Flow` class to perform the same flow with different methods of |
| 303 | + presenting the authorization URL to the user or obtaining the authorization |
| 304 | + response, such as using an embedded web view. |
| 305 | +
|
| 306 | + .. _Installed Application Authorization Flow: |
| 307 | + https://developers.google.com/api-client-library/python/auth |
| 308 | + /installed-app |
| 309 | + """ |
| 310 | + _OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' |
| 311 | + |
| 312 | + _DEFAULT_AUTH_PROMPT_MESSAGE = ( |
| 313 | + 'Please visit this URL to authorize this application: {url}') |
| 314 | + """str: The message to display when prompting the user for |
| 315 | + authorization.""" |
| 316 | + _DEFAULT_AUTH_CODE_MESSAGE = ( |
| 317 | + 'Enter the authorization code: ') |
| 318 | + """str: The message to display when prompting the user for the |
| 319 | + authorization code. Used only by the console strategy.""" |
| 320 | + |
| 321 | + _DEFAULT_WEB_SUCCESS_MESSAGE = ( |
| 322 | + 'The authentication flow has completed, you may close this window.') |
| 323 | + |
| 324 | + def run_console( |
| 325 | + self, |
| 326 | + authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, |
| 327 | + authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE, |
| 328 | + **kwargs): |
| 329 | + """Run the flow using the console strategy. |
| 330 | +
|
| 331 | + The console strategy instructs the user to open the authorization URL |
| 332 | + in their browser. Once the authorization is complete the authorization |
| 333 | + server will give the user a code. The user then must copy & paste this |
| 334 | + code into the application. The code is then exchanged for a token. |
| 335 | +
|
| 336 | + Args: |
| 337 | + authorization_prompt_message (str): The message to display to tell |
| 338 | + the user to navigate to the authorization URL. |
| 339 | + authorization_code_message (str): The message to display when |
| 340 | + prompting the user for the authorization code. |
| 341 | + kwargs: Additional keyword arguments passed through to |
| 342 | + :meth:`authorization_url`. |
| 343 | +
|
| 344 | + Returns: |
| 345 | + google.oauth2.credentials.Credentials: The OAuth 2.0 credentials |
| 346 | + for the user. |
| 347 | + """ |
| 348 | + kwargs.setdefault('prompt', 'consent') |
| 349 | + |
| 350 | + self.redirect_uri = self._OOB_REDIRECT_URI |
| 351 | + |
| 352 | + auth_url, _ = self.authorization_url(**kwargs) |
| 353 | + |
| 354 | + print(authorization_prompt_message.format(url=auth_url)) |
| 355 | + |
| 356 | + code = input(authorization_code_message) |
| 357 | + |
| 358 | + self.fetch_token(code=code) |
| 359 | + |
| 360 | + return self.credentials |
| 361 | + |
| 362 | + def run_local_server( |
| 363 | + self, host='localhost', port=8080, |
| 364 | + authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, |
| 365 | + success_message=_DEFAULT_WEB_SUCCESS_MESSAGE, |
| 366 | + open_browser=True, |
| 367 | + **kwargs): |
| 368 | + """Run the flow using the server strategy. |
| 369 | +
|
| 370 | + The server strategy instructs the user to open the authorization URL in |
| 371 | + their browser and will attempt to automatically open the URL for them. |
| 372 | + It will start a local web server to listen for the authorization |
| 373 | + response. Once authorization is complete the authorization server will |
| 374 | + redirect the user's browser to the local web server. The web server |
| 375 | + will get the authorization code from the response and shutdown. The |
| 376 | + code is then exchanged for a token. |
| 377 | +
|
| 378 | + Args: |
| 379 | + host (str): The hostname for the local redirect server. This will |
| 380 | + be served over http, not https. |
| 381 | + port (int): The port for the local redirect server. |
| 382 | + authorization_prompt_message (str): The message to display to tell |
| 383 | + the user to navigate to the authorization URL. |
| 384 | + success_message (str): The message to display in the web browser |
| 385 | + the authorization flow is complete. |
| 386 | + open_browser (bool): Whether or not to open the authorization URL |
| 387 | + in the user's browser. |
| 388 | + kwargs: Additional keyword arguments passed through to |
| 389 | + :meth:`authorization_url`. |
| 390 | +
|
| 391 | + Returns: |
| 392 | + google.oauth2.credentials.Credentials: The OAuth 2.0 credentials |
| 393 | + for the user. |
| 394 | + """ |
| 395 | + self.redirect_uri = 'http://{}:{}/'.format(host, port) |
| 396 | + |
| 397 | + auth_url, _ = self.authorization_url(**kwargs) |
| 398 | + |
| 399 | + wsgi_app = _RedirectWSGIApp(success_message) |
| 400 | + local_server = wsgiref.simple_server.make_server( |
| 401 | + host, port, wsgi_app, handler_class=_WSGIRequestHandler) |
| 402 | + |
| 403 | + if open_browser: |
| 404 | + webbrowser.open(auth_url, new=1, autoraise=True) |
| 405 | + |
| 406 | + print(authorization_prompt_message.format(url=auth_url)) |
| 407 | + |
| 408 | + local_server.handle_request() |
| 409 | + |
| 410 | + # Note: using https here because oauthlib is very picky that |
| 411 | + # OAuth 2.0 should only occur over https. |
| 412 | + authorization_response = wsgi_app.last_request_uri.replace( |
| 413 | + 'http', 'https') |
| 414 | + self.fetch_token(authorization_response=authorization_response) |
| 415 | + |
| 416 | + return self.credentials |
| 417 | + |
| 418 | + |
| 419 | +class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler): |
| 420 | + """Custom WSGIRequestHandler. |
| 421 | +
|
| 422 | + Uses a named logger instead of printing to stderr. |
| 423 | + """ |
| 424 | + def log_message(self, format, *args, **kwargs): |
| 425 | + # pylint: disable=redefined-builtin |
| 426 | + # (format is the argument name defined in the superclass.) |
| 427 | + _LOGGER.info(format, *args, **kwargs) |
| 428 | + |
| 429 | + |
| 430 | +class _RedirectWSGIApp(object): |
| 431 | + """WSGI app to handle the authorization redirect. |
| 432 | +
|
| 433 | + Stores the request URI and displays the given success message. |
| 434 | + """ |
| 435 | + |
| 436 | + def __init__(self, success_message): |
| 437 | + """ |
| 438 | + Args: |
| 439 | + success_message (str): The message to display in the web browser |
| 440 | + the authorization flow is complete. |
| 441 | + """ |
| 442 | + self.last_request_uri = None |
| 443 | + self._success_message = success_message |
| 444 | + |
| 445 | + def __call__(self, environ, start_response): |
| 446 | + """WSGI Callable. |
| 447 | +
|
| 448 | + Args: |
| 449 | + environ (Mapping[str, Any]): The WSGI environment. |
| 450 | + start_response (Callable[str, list]): The WSGI start_response |
| 451 | + callable. |
| 452 | +
|
| 453 | + Returns: |
| 454 | + Iterable[bytes]: The response body. |
| 455 | + """ |
| 456 | + start_response('200 OK', [('Content-type', 'text/plain')]) |
| 457 | + self.last_request_uri = wsgiref.util.request_uri(environ) |
| 458 | + return [self._success_message.encode('utf-8')] |
0 commit comments