Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 41a2bba

Browse files
author
Jon Wayne Parrott
authored
Add InstalledAppFlow (#128)
1 parent 06a27e8 commit 41a2bba

4 files changed

Lines changed: 371 additions & 86 deletions

File tree

additional_packages/google_auth_oauthlib/google_auth_oauthlib/flow.py

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
This module provides integration with `requests-oauthlib`_ for running the
1818
`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.
1919
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
2121
authorization flow::
2222
2323
from google_auth_oauthlib.flow import Flow
@@ -44,19 +44,30 @@
4444
session = flow.authorized_session()
4545
print(session.get('https://www.googleapis.com/userinfo/v2/me').json())
4646
47+
This particular flow can be handled entirely by using
48+
:class:`InstalledAppFlow`.
49+
4750
.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
4851
.. _OAuth 2.0 Authorization Flow:
4952
https://tools.ietf.org/html/rfc6749#section-1.2
5053
"""
5154

5255
import json
56+
import logging
57+
import webbrowser
58+
import wsgiref.simple_server
59+
import wsgiref.util
5360

5461
import google.auth.transport.requests
5562
import google.oauth2.credentials
63+
from six.moves import input
5664

5765
import google_auth_oauthlib.helpers
5866

5967

68+
_LOGGER = logging.getLogger(__name__)
69+
70+
6071
class Flow(object):
6172
"""OAuth 2.0 Authorization Flow
6273
@@ -253,3 +264,195 @@ class using this flow's :attr:`credentials`.
253264
"""
254265
return google.auth.transport.requests.AuthorizedSession(
255266
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

Comments
 (0)