Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add InstalledAppFlow #128

Merged
merged 7 commits into from
Mar 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 204 additions & 1 deletion additional_packages/google_auth_oauthlib/google_auth_oauthlib/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
This module provides integration with `requests-oauthlib`_ for running the
`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.

Here's an example of using the flow with the installed application
Here's an example of using :class:`Flow` with the installed application
authorization flow::

from google_auth_oauthlib.flow import Flow
Expand All @@ -44,19 +44,30 @@
session = flow.authorized_session()
print(session.get('https://www.googleapis.com/userinfo/v2/me').json())

This particular flow can be handled entirely by using
:class:`InstalledAppFlow`.

.. _requests-oauthlib: http:https://requests-oauthlib.readthedocs.io/en/stable/
.. _OAuth 2.0 Authorization Flow:
https://tools.ietf.org/html/rfc6749#section-1.2
"""

import json
import logging
import webbrowser
import wsgiref.simple_server
import wsgiref.util

import google.auth.transport.requests
import google.oauth2.credentials
from six.moves import input

import google_auth_oauthlib.helpers


_LOGGER = logging.getLogger(__name__)


class Flow(object):
"""OAuth 2.0 Authorization Flow

Expand Down Expand Up @@ -253,3 +264,195 @@ class using this flow's :attr:`credentials`.
"""
return google.auth.transport.requests.AuthorizedSession(
self.credentials)


class InstalledAppFlow(Flow):
"""Authorization flow helper for installed applications.

This :class:`Flow` subclass makes it easier to perform the
`Installed Application Authorization Flow`_. This flow is useful for
local development or applications that are installed on a desktop operating
system.

This flow has two strategies: The console strategy provided by
:meth:`run_console` and the local server strategy provided by
:meth:`run_local_server`.

Example::

from google_auth_oauthlib.flow import InstalledAppFlow

flow = InstalledAppFlow.from_client_secrets_file(
'client_secrets.json',
scopes=['profile', 'email'])

flow.run_local_server()

session = flow.authorized_session()

profile_info = session.get(
'https://www.googleapis.com/userinfo/v2/me').json()

print(profile_info)
# {'name': '...', 'email': '...', ...}


Note that these aren't the only two ways to accomplish the installed
application flow, they are just the most common ways. You can use the
:class:`Flow` class to perform the same flow with different methods of
presenting the authorization URL to the user or obtaining the authorization
response, such as using an embedded web view.

.. _Installed Application Authorization Flow:
https://developers.google.com/api-client-library/python/auth
/installed-app
"""
_OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'

_DEFAULT_AUTH_PROMPT_MESSAGE = (
'Please visit this URL to authorize this application: {url}')
"""str: The message to display when prompting the user for
authorization."""
_DEFAULT_AUTH_CODE_MESSAGE = (
'Enter the authorization code: ')
"""str: The message to display when prompting the user for the
authorization code. Used only by the console strategy."""

_DEFAULT_WEB_SUCCESS_MESSAGE = (
'The authentication flow has completed, you may close this window.')

def run_console(
self,
authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE,
**kwargs):
"""Run the flow using the console strategy.

The console strategy instructs the user to open the authorization URL
in their browser. Once the authorization is complete the authorization
server will give the user a code. The user then must copy & paste this
code into the application. The code is then exchanged for a token.

Args:
authorization_prompt_message (str): The message to display to tell
the user to navigate to the authorization URL.
authorization_code_message (str): The message to display when
prompting the user for the authorization code.
kwargs: Additional keyword arguments passed through to
:meth:`authorization_url`.

Returns:
google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
for the user.
"""
kwargs.setdefault('prompt', 'consent')

self.redirect_uri = self._OOB_REDIRECT_URI

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

print(authorization_prompt_message.format(url=auth_url))

This comment was marked as spam.


code = input(authorization_code_message)

self.fetch_token(code=code)

return self.credentials

def run_local_server(
self, host='localhost', port=8080,
authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
success_message=_DEFAULT_WEB_SUCCESS_MESSAGE,
open_browser=True,
**kwargs):
"""Run the flow using the server strategy.

The server strategy instructs the user to open the authorization URL in
their browser and will attempt to automatically open the URL for them.
It will start a local web server to listen for the authorization
response. Once authorization is complete the authorization server will
redirect the user's browser to the local web server. The web server
will get the authorization code from the response and shutdown. The
code is then exchanged for a token.

Args:
host (str): The hostname for the local redirect server. This will
be served over http, not https.
port (int): The port for the local redirect server.
authorization_prompt_message (str): The message to display to tell
the user to navigate to the authorization URL.
success_message (str): The message to display in the web browser
the authorization flow is complete.
open_browser (bool): Whether or not to open the authorization URL
in the user's browser.
kwargs: Additional keyword arguments passed through to
:meth:`authorization_url`.

Returns:
google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
for the user.
"""
self.redirect_uri = 'http:https://{}:{}/'.format(host, port)

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

wsgi_app = _RedirectWSGIApp(success_message)
local_server = wsgiref.simple_server.make_server(
host, port, wsgi_app, handler_class=_WSGIRequestHandler)

if open_browser:
webbrowser.open(auth_url, new=1, autoraise=True)

print(authorization_prompt_message.format(url=auth_url))

local_server.handle_request()

# Note: using https here because oauthlib is very picky that
# OAuth 2.0 should only occur over https.
authorization_response = wsgi_app.last_request_uri.replace(
'http', 'https')
self.fetch_token(authorization_response=authorization_response)

return self.credentials


class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
"""Custom WSGIRequestHandler.

Uses a named logger instead of printing to stderr.
"""
def log_message(self, format, *args, **kwargs):
# pylint: disable=redefined-builtin
# (format is the argument name defined in the superclass.)
_LOGGER.info(format, *args, **kwargs)


class _RedirectWSGIApp(object):
"""WSGI app to handle the authorization redirect.

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

def __init__(self, success_message):
"""
Args:
success_message (str): The message to display in the web browser
the authorization flow is complete.
"""
self.last_request_uri = None
self._success_message = success_message

def __call__(self, environ, start_response):
"""WSGI Callable.

Args:
environ (Mapping[str, Any]): The WSGI environment.
start_response (Callable[str, list]): The WSGI start_response
callable.

Returns:
Iterable[bytes]: The response body.
"""
start_response('200 OK', [('Content-type', 'text/plain')])
self.last_request_uri = wsgiref.util.request_uri(environ)
return [self._success_message.encode('utf-8')]
Loading