Add google.oauth2.flow - utility for doing OAuth 2.0 Authorization Flow (#100)
diff --git a/google/oauth2/flow.py b/google/oauth2/flow.py
new file mode 100644
index 0000000..69e73ff
--- /dev/null
+++ b/google/oauth2/flow.py
@@ -0,0 +1,250 @@
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""OAuth 2.0 Authorization Flow
+
+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
+authorization flow::
+
+ import google.oauth2.flow
+
+ # Create the flow using the client secrets file from the Google API
+ # Console.
+ flow = google.oauth2.flow.Flow.from_client_secrets_file(
+ 'path/to/client_secrets.json',
+ scopes=['profile', 'email'],
+ redirect_uri='urn:ietf:wg:oauth:2.0:oob')
+
+ # Tell the user to go to the authorization URL.
+ auth_url, _ = flow.authorization_url(prompt='consent')
+
+ print('Please go to this URL: {}'.format(auth_url))
+
+ # The user will get an authorization code. This code is used to get the
+ # access token.
+ code = input('Enter the authorization code: ')
+ flow.fetch_token(code=code)
+
+ # You can use flow.credentials, or you can just get a requests session
+ # using flow.authorized_session.
+ session = flow.authorized_session()
+ print(session.get('https://www.googleapis.com/userinfo/v2/me').json())
+
+.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
+.. _OAuth 2.0 Authorization Flow:
+ https://tools.ietf.org/html/rfc6749#section-1.2
+"""
+
+import json
+
+import requests_oauthlib
+
+import google.auth.transport.requests
+import google.oauth2.credentials
+
+_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
+
+
+class Flow(object):
+ """OAuth 2.0 Authorization Flow
+
+ This class uses a :class:`requests_oauthlib.OAuth2Session` instance at
+ :attr:`oauth2session` to perform all of the OAuth 2.0 logic. This class
+ just provides convenience methods and sane defaults for doing Google's
+ particular flavors of OAuth 2.0.
+
+ Typically you'll construct an instance of this flow using
+ :meth:`from_client_secrets_file` and a `client secrets file`_ obtained
+ from the `Google API Console`_.
+
+ .. _client secrets file:
+ https://developers.google.com/identity/protocols/OAuth2WebServer
+ #creatingcred
+ .. _Google API Console:
+ https://console.developers.google.com/apis/credentials
+ """
+
+ def __init__(self, client_config, scopes, **kwargs):
+ """
+ Args:
+ client_config (Mapping[str, Any]): The client
+ configuration in the Google `client secrets`_ format.
+ scopes (Sequence[str]): The list of scopes to request during the
+ flow.
+ kwargs: Any additional parameters passed to
+ :class:`requests_oauthlib.OAuth2Session`
+
+ Raises:
+ ValueError: If the client configuration is not in the correct
+ format.
+
+ .. _client secrets:
+ https://developers.google.com/api-client-library/python/guide
+ /aaa_client_secrets
+ """
+ self.client_config = None
+ """Mapping[str, Any]: The OAuth 2.0 client configuration."""
+ self.client_type = None
+ """str: The client type, either ``'web'`` or ``'installed'``"""
+
+ if 'web' in client_config:
+ self.client_config = client_config['web']
+ self.client_type = 'web'
+ elif 'installed' in client_config:
+ self.client_config = client_config['installed']
+ self.client_type = 'installed'
+ else:
+ raise ValueError(
+ 'Client secrets must be for a web or installed app.')
+
+ if not _REQUIRED_CONFIG_KEYS.issubset(self.client_config.keys()):
+ raise ValueError('Client secrets is not in the correct format.')
+
+ self.oauth2session = requests_oauthlib.OAuth2Session(
+ client_id=self.client_config['client_id'],
+ scope=scopes,
+ **kwargs)
+ """requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
+
+ @classmethod
+ def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
+ """Creates a :class:`Flow` instance from a Google client secrets file.
+
+ Args:
+ client_secrets_file (str): The path to the client secrets .json
+ file.
+ scopes (Sequence[str]): The list of scopes to request during the
+ flow.
+ kwargs: Any additional parameters passed to
+ :class:`requests_oauthlib.OAuth2Session`
+
+ Returns:
+ Flow: The constructed Flow instance.
+ """
+ with open(client_secrets_file, 'r') as json_file:
+ client_config = json.load(json_file)
+
+ return cls(client_config, scopes=scopes, **kwargs)
+
+ @property
+ def redirect_uri(self):
+ """The OAuth 2.0 redirect URI. Pass-through to
+ ``self.oauth2session.redirect_uri``."""
+ return self.oauth2session.redirect_uri
+
+ @redirect_uri.setter
+ def redirect_uri(self, value):
+ self.oauth2session.redirect_uri = value
+
+ def authorization_url(self, **kwargs):
+ """Generates an authorization URL.
+
+ This is the first step in the OAuth 2.0 Authorization Flow. The user's
+ browser should be redirected to the returned URL.
+
+ This method calls
+ :meth:`requests_oauthlib.OAuth2Session.authorization_url`
+ and specifies the client configuration's authorization URI (usually
+ Google's authorization server) and specifies that "offline" access is
+ desired. This is required in order to obtain a refresh token.
+
+ Args:
+ kwargs: Additional arguments passed through to
+ :meth:`requests_oauthlib.OAuth2Session.authorization_url`
+
+ Returns:
+ Tuple[str, str]: The generated authorization URL and state. The
+ user must visit the URL to complete the flow. The state is used
+ when completing the flow to verify that the request originated
+ from your application. If your application is using a different
+ :class:`Flow` instance to obtain the token, you will need to
+ specify the ``state`` when constructing the :class:`Flow`.
+ """
+ url, state = self.oauth2session.authorization_url(
+ self.client_config['auth_uri'],
+ access_type='offline', **kwargs)
+
+ return url, state
+
+ def fetch_token(self, **kwargs):
+ """Completes the Authorization Flow and obtains an access token.
+
+ This is the final step in the OAuth 2.0 Authorization Flow. This is
+ called after the user consents.
+
+ This method calls
+ :meth:`requests_oauthlib.OAuth2Session.fetch_token`
+ and specifies the client configuration's token URI (usually Google's
+ token server).
+
+ Args:
+ kwargs: Arguments passed through to
+ :meth:`requests_oauthlib.OAuth2Session.fetch_token`. At least
+ one of ``code`` or ``authorization_response`` must be
+ specified.
+
+ Returns:
+ Mapping[str, str]: The obtained tokens. Typically, you will not use
+ return value of this function and instead and use
+ :meth:`credentials` to obtain a
+ :class:`~google.auth.credentials.Credentials` instance.
+ """
+ return self.oauth2session.fetch_token(
+ self.client_config['token_uri'],
+ client_secret=self.client_config['client_secret'],
+ **kwargs)
+
+ @property
+ def credentials(self):
+ """Returns credentials from the OAuth 2.0 session.
+
+ :meth:`fetch_token` must be called before accessing this. This method
+ constructs a :class:`google.oauth2.credentials.Credentials` class using
+ the session's token and the client config.
+
+ Returns:
+ google.oauth2.credentials.Credentials: The constructed credentials.
+
+ Raises:
+ ValueError: If there is no access token in the session.
+ """
+ if not self.oauth2session.token:
+ raise ValueError(
+ 'There is no access token for this session, did you call '
+ 'fetch_token?')
+
+ return google.oauth2.credentials.Credentials(
+ self.oauth2session.token['access_token'],
+ refresh_token=self.oauth2session.token['refresh_token'],
+ token_uri=self.client_config['token_uri'],
+ client_id=self.client_config['client_id'],
+ client_secret=self.client_config['client_secret'],
+ scopes=self.oauth2session.scope)
+
+ def authorized_session(self):
+ """Returns a :class:`requests.Session` authorized with credentials.
+
+ :meth:`fetch_token` must be called before this method. This method
+ constructs a :class:`google.auth.transport.requests.AuthorizedSession`
+ class using this flow's :attr:`credentials`.
+
+ Returns:
+ google.auth.transport.requests.AuthorizedSession: The constructed
+ session.
+ """
+ return google.auth.transport.requests.AuthorizedSession(
+ self.credentials)