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)