Merge commit 'ae733129' into oauth2client v3.0.0 Initial commitcd  of oauth2client v3.0.0 with history.
am: 1af3acd3c3

Change-Id: Ie05565822188daa03fbda84f568328e7fb090694
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..0151e07
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,12 @@
+[report]
+omit =
+    */samples/*
+    # Don't report coverage over platform-specific modules.
+    oauth2client/contrib/_fcntl_opener.py
+    oauth2client/contrib/_win32_opener.py
+    oauth2client/contrib/django_util/apps.py
+exclude_lines =
+    # Re-enable the standard pragma
+    pragma: NO COVER
+    # Ignore debug-only repr
+    def __repr__
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89c1121
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+# Build artifacts
+*.py[cod]
+oauth2client.egg-info/
+build/
+dist/
+
+# Documentation-related
+docs/_build
+/google_appengine/
+
+# Test files
+.tox/
+
+# Django test database
+db.sqlite3
+
+# Coverage files
+.coverage
+coverage.xml
+nosetests.xml
+htmlcov/
+
+# Files with private / local data
+scripts/local_test_setup
+tests/data/key.json
+tests/data/key.p12
+tests/data/user-key.json
+
+# PyCharm configuration:
+.idea
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a8c01fa
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,43 @@
+language: python
+python: 2.7
+sudo: false
+# TODO(issue 532): Fix syntax when 3.5 is natively available upstream
+matrix:
+  include:
+    - python: 3.5
+      env:
+      - TOX_ENV=py35
+env:
+  matrix:
+  - TOX_ENV=py26
+  - TOX_ENV=py27
+  - TOX_ENV=py33
+  - TOX_ENV=py34
+  - TOX_ENV=pypy
+  - TOX_ENV=docs
+  - TOX_ENV=system-tests
+  - TOX_ENV=system-tests3
+  - TOX_ENV=gae
+  - TOX_ENV=flake8
+  global:
+  - GAE_PYTHONPATH=${HOME}/.cache/google_appengine
+cache:
+  directories:
+  - ${HOME}/.cache
+install:
+- ./scripts/install.sh
+script:
+- ./scripts/run.sh
+after_success:
+- if [[ "${TOX_ENV}" == "gae" ]]; then tox -e coveralls; fi
+notifications:
+  email: false
+
+deploy:
+  provider: pypi
+  user: gcloudpypi
+  password:
+    secure: "C9ImNa5kbdnrQNfX9ww4PUtQIr3tN+nfxl7eDkP1B8Qr0QNYjrjov7x+DLImkKvmoJd3dxYtYIpLE9esObUHu0gKHYxqymNHtuAAyoBOUfPtmp0vIEse9brNKMtaey5Ngk7ZWz9EHKBBqRHxqgN+Giby+K9Ta3K3urJIq6urYhE="
+  on:
+    tags: true
+    repo: google/oauth2client
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..bf33dea
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,425 @@
+# CHANGELOG
+
+## v3.0.0
+
+* Populate `token_expiry` for GCE credentials. (#473)
+* Move GCE metadata interface to a separate module. (#520)
+* Populate `scopes` for GCE credentials. (#524)
+* Fix Python 3.5 compatibility. (#531)
+* Add `oauth2client.contrib.sqlalchemy`, a SQLAlchemy-based credential store. (#527)
+* Improve error when an invalid client secret is provided. (#530)
+* Add `oauth2client.contrib.multiprocess_storage`. This supersedes the functionality in `oauth2client.contrib.multistore_file`. (#504)
+* Pull httplib2 usage into a separate transport module. (#559, #561)
+* Refactor all django-related code into `oauth2client.contrib.django_util`. Add `DjangoORMStorage`, remove `FlowField`. (#546)
+* Fix application default credentials resolution order. (#570)
+* Add configurable timeout for GCE metadata server check. (#571)
+* Add warnings when using deprecated `approval_prompt='force'`. (#572)
+* Add deprecation warning to `oauth2client.contrib.multistore_file`. (#574)
+* (Hygiene) PEP8 compliance and various style fixes (#537, #540, #552, #562)
+* (Hygiene) Remove duplicated exception classes in `oauth2client.contrib.appengine`. (#533)
+
+NOTE: The next major release of oauth2client (v4.0.0) will remove the `oauth2client.contrib.multistore_file` module.
+
+## v2.2.0
+
+* Added support to override `token_uri` and `revoke_uri` in `oauth2client.service_account.ServiceAccountCredentials`. (#510)
+* `oauth2client.contrib.multistore_file` now handles `OSError` in addition to `IOError` because Windows may raise `OSError` where other platforms will raise `IOError`.
+* `oauth2client.contrib.django_util` and `oauth2client.contrib.django_orm` have been updated to support Django 1.8 - 1.10. Versions of Django below 1.8 will not work with these modules.
+
+## v2.1.0
+
+* Add basic support for JWT access credentials. (#503)
+* Fix `oauth2client.client.DeviceFlowInfo` to use UTC instead of the system timezone when calculating code expiration.
+
+## v2.0.2
+
+* Fix issue where `flask_util.UserOAuth2.required` would accept expired credentials (#452).
+* Fix issue where `flask_util` would fill the session with `Flow` objects (#498).
+* Fix issue with Python 3 binary strings in `Flow.step2_exchange` (#446).
+* Improve test coverage to 100%.
+
+## v2.0.1
+
+* Making scopes optional on Google Compute Engine `AppAssertionCredentials`
+  and adding a warning that GCE won't honor scopes (#419)
+* Adding common `sign_blob()` to service account types and a
+  `service_account_email` property. (#421)
+* Improving error message in P12 factory
+  `ServiceAccountCredentials.from_p12_keyfile` when pyOpenSSL is
+  missing. (#424)
+* Allowing default flags in `oauth2client.tools.run_flow()`
+  rather than forcing users to create a dummy argparser (#426)
+* Removing `oauth2client.util.dict_to_tuple_key()` from public
+  interface (#429)
+* Adding `oauth2client.contrib._appengine_ndb` helper module
+  for `oauth2client.contrib.appengine` and moving most code that
+  uses the `ndb` library into the helper (#434)
+* Fix error in `django_util` sample code (#438)
+
+## v2.0.0-post1
+
+* Fix Google Compute Engine breakage (#411, breakage introduced in #387) that
+  made it impossible to obtain access tokens
+* Implement `ServiceAccountCredentials.from_p12_keyfile_buffer()`
+  to allow passing a file-like object in addition to the factory
+  constructor that uses a filename directly (#413)
+* Implement `ServiceAccountCredentials.create_delegated()`
+  to allow upgrading a credential to one that acts on behalf
+  of a given subject (#420)
+
+## v2.0.0
+
+* Add django_util (#332)
+* Avoid OAuth2Credentials `id_token` going out of sync after a token
+  refresh (#337)
+* Move to a `contrib` sub-package code not considered a core part of
+  the library (#346, #353, #370, #375, #376, #382)
+* Add `token_expiry` to `devshell` credentials (#372)
+* Move `Storage` locking into a base class (#379)
+* Added dictionary storage (#380)
+* Added `to_json` and `from_json` methods to all `Credentials`
+  classes (#385)
+* Fall back to read-only credentials on EACCES errors (#389)
+* Coalesced the two `ServiceAccountCredentials`
+  classes (#395, #396, #397, #398, #400)
+
+### Special Note About `ServiceAccountCredentials`:
+-------------------------------------------------
+
+For JSON keys, you can create a credential via
+
+```py
+from oauth2client.service_account import ServiceAccountCredentials
+credentials = ServiceAccountCredentials.from_json_keyfile_name(
+    key_file_name, scopes=[...])
+```
+
+You can still rely on
+
+```py
+from oauth2client.client import GoogleCredentials
+credentials = GoogleCredentials.get_application_default()
+```
+
+returning these credentials when you set the `GOOGLE_APPLICATION_CREDENTIALS`
+environment variable.
+
+For `.p12` keys, construct via
+
+```py
+credentials = ServiceAccountCredentials.from_p12_keyfile(
+    service_account_email, key_file_name, scopes=[...])
+```
+
+though we urge you to use JSON keys (rather than `.p12` keys) if you can.
+
+This is equivalent to the previous method
+
+```py
+# PRE-oauth2client 2.0.0 EXAMPLE CODE!
+from oauth2client.client import SignedJwtAssertionCredentials
+
+with open(key_file_name, 'rb') as key_file:
+    private_key = key_file.read()
+
+credentials = SignedJwtAssertionCredentials(
+    service_account_email, private_key, scope=[...])
+```
+
+## v1.5.2
+
+* Add access token refresh error class that includes HTTP status (#310)
+* Python3 compatibility fixes for Django (#316, #318)
+* Fix incremental auth in flask_util (#322)
+* Fall back to credential refresh on EDEADLK in multistore_file (#336)
+
+## v1.5.1
+
+* Fix bad indent in `tools.run_flow()` (#301, bug was
+  introduced when switching from 2 space indents to 4)
+
+## v1.5.0
+
+* Fix (more like clarify) `bytes` / `str` handling in crypto
+  methods. (#203, #250, #272)
+* Replacing `webapp` with `webapp2` in `oauth2client.appengine` (#217)
+* Added optional `state` parameter to
+  `step1_get_authorize_url`. (#219 and #222)
+* Added `flask_util` module that provides a Flask extension to aid
+  with using OAuth2 web server flow. This provides the same functionality
+  as the `appengine.webapp2` OAuth2Decorator, but will work with any Flask
+  application regardless of hosting environment. (#226, #273)
+* Track scopes used on credentials objects (#230)
+* Moving docs to [readthedocs.org][1] (#237, #238, #244)
+* Removing `old_run` module. Was deprecated July 2, 2013. (#285)
+* Avoid proxies when querying for GCE metadata (to check if
+  running on GCE) (#114, #293)
+
+[1]: https://readthedocs.org/
+
+## v1.4.12
+
+* Fix OS X flaky test failure (#189).
+* Fix broken OpenSSL import (#191).
+* Remove `@util.positional` from wrapped request in `Credentials.authorize()`
+  (#196, #197).
+* Changing pinned dependencies to `>=` (#200, #204).
+* Support client authentication using `Authorization` header (#206).
+* Clarify environment check in case where GAE imports succeed but GAE services
+  aren't available (#208).
+
+## v1.4.11
+
+* Better environment detection with Managed VMs.
+* Better OpenSSL detection in exotic environments.
+
+## v1.4.10
+
+* Update the `OpenSSL` check to be less strict about finding `crypto.py` in
+  the `OpenSSL` directory.
+* `tox` updates for new environment handling in `tox`.
+
+## v1.4.9
+
+* Ensure that the ADC fails if we try to *write* the well-known file to a
+  directory that doesn't exist, but not if we try to *read* from one.
+
+## v1.4.8
+
+* Better handling of `body` during token refresh when `body` is a stream.
+* Better handling of expired tokens in storage.
+* Cleanup around `openSSL` import.
+* Allow custom directory for the `well_known_file`.
+* Integration tests for python2 and python3. (!!!)
+* Stricter file permissions when saving the `well_known_file`.
+* Test cleanup around config file locations.
+
+## v1.4.7
+
+* Add support for Google Developer Shell credentials.
+* Better handling of filesystem errors in credential refresh.
+* python3 fixes
+* Add `NO_GCE_CHECK` for skipping GCE detection.
+* Better error messages on `InvalidClientSecretsError`.
+* Comment cleanup on `run_flow`.
+
+## v1.4.6
+
+* Add utility function to convert PKCS12 key to PEM. (#115)
+* Change GCE detection logic. (#93)
+* Add a tox env for doc generation.
+
+## v1.4.5
+
+* Set a shorter timeout for an Application Default Credentials issue on some
+  networks. (#93, #101)
+* Test cleanup, switch from mox to mock. (#103)
+* Switch docs to sphinx from epydoc.
+
+## v1.4.4
+
+* Fix a bug in bytes/string encoding of headers.
+
+## v1.4.3
+
+* Big thanks to @dhermes for spotting and fixing a mess in our test setup.
+
+* Fix a serious issue with tests not being run. (#86, #87, #89)
+* Start credentials cleanup for single 2LO/3LO call. (#83, #84)
+* Clean up stack traces when re-raising in some places. (#79)
+* Clean up doc building. (#81, #82)
+* Fixed minimum version for `six` dependency. (#75)
+
+## v1.4.2
+
+* Several small bugfixes related to `six`/py3 support.
+
+## v1.4.1
+
+* Fix a critical bug on import in `oauth2client.tools`.
+
+## v1.4
+
+* Merge python3 branch! Massive thanks due to @pferate and @methane for doing
+  the heavy lifting.
+
+* Make `oauth2client.tools` import gracefully if `argparse` isn't present.
+
+* Change `flow.step2_exchange` to preserve the raw `id_token` in the
+  `token_response` field.
+
+## v1.3.2
+
+* Quick bugfix for an issue with dict-like arguments to `flow.step2_exchange`,
+  which is common in some environments (such as GAE).
+
+## v1.3.1
+
+* Quick bugfix for bad error handling in from_json.
+
+## v1.3
+
+* Added support for the
+  [Google Application Default Credentials](https://developers.google.com/accounts/docs/application-default-credentials)
+  for more information (thanks @orestica).
+* Added support for OAuth2 for devices (#3, thanks @sde-melo).
+* The minimum required Python version is now 2.6.
+* The `anyjson` submodule has been removed.
+
+* Better exception handling around missing crypto libraries (#56).
+* Improve error messages in `AccessTokenRefreshError` (#53, thanks
+  @erickoledadevrel).
+* Drop `uritemplate` as a dependency.
+* Handle X509 certs with PyCrypto (#51, thanks @liujin-google).
+* Handle additional failure types on OSX (#32, thanks @simoncadman).
+* Better unicode handling with PKCS12 passwords (#31, thanks @jterrace).
+* Better retry handling with bad server replies on refresh (#29, thanks
+  @kaste).
+* Better logging for missing `refresh_token` in server replies (#21).
+* Support `login_hint` (#18, thanks @jay0lee).
+* Better overwrite options in `django_orm.Storage`. (#2, thanks @lraccomando).
+
+
+## v1.2
+
+* The use of the `gflags` library is now deprecated, and is no longer a
+  dependency. If you are still using the `oauth2client.tools.run()` function
+  then include `python-gflags` as a dependency of your application or switch to
+  `oauth2client.tools.run_flow`.
+* Samples have been updated to use the new `apiclient.sample_tools`, and no
+  longer use `gflags`.
+* Added support for the experimental Object Change Notification, as found in
+  the Cloud Storage API.
+* The oauth2client App Engine decorators are now threadsafe.
+
+* Use the following redirects feature of httplib2 where it returns the
+  ultimate URL after a series of redirects to avoid multiple hops for every
+  resumable media upload request.
+* Updated AdSense Management API samples to V1.3
+* Add option to automatically retry requests.
+* Ability to list registered keys in `multistore_file`.
+* User-agent must contain `(gzip)`.
+* The `method` parameter for `httplib2` is not positional. This would cause
+  spurious warnings in the logging.
+* Making OAuth2Decorator more extensible. Fixes Issue 256.
+* Update AdExchange Buyer API examples to version v1.2.
+
+
+## v1.1
+
+* Add PEM support to `SignedJWTAssertionCredentials` (used to only support
+  PKCS12 formatted keys). Note that if you use PEM formatted keys you can use
+  PyCrypto 2.6 or later instead of OpenSSL.
+
+* Allow deserialized discovery docs to be passed to `build_from_document()`.
+
+* Make `ResumableUploadError` derive from `HttpError`.
+* Many changes to move all the closures in `apiclient.discovery` into real
+  classes and objects.
+* Make `from_json` behavior inheritable.
+* Expose the full token response in `OAuth2Client` and `OAuth2Decorator`.
+* Handle reasons that are None.
+* Added support for NDB based storing of oauth2client objects.
+* Update `grant_type` for `AssertionCredentials`.
+* Adding a `.revoke()` to Credentials. Closes issue 98.
+* Modify `oauth2client.multistore_file` to store and retrieve credentials
+  using an arbitrary key.
+* Don't accept `403` challenges by default for auth challenges.
+* Set `httplib2.RETRIES` to 1.
+* Consolidate handling of scopes.
+* Upgrade to httplib2 version 0.8.
+* Allow setting the `response_type` in `OAuth2WebServerFlow`.
+* Ensure that `dataWrapper` feature is checked before using the `data` value.
+* HMAC verification does not use a constant time algorithm.
+
+## v1.0
+
+* Changes to the code for running tests and building releases.
+
+## v1.0c3
+
+* In samples and oauth2 decorator, escape untrusted content before displaying it.
+* Do not allow credentials files to be symlinks.
+* Add XSRF protection to oauth2decorator callback state.
+* Handle uploading chunked media by stream.
+* Handle passing streams directly to httplib2.
+* Add support for Google Compute Engine service accounts.
+* Flows no longer need to be saved between uses.
+* Change GET to POST if URI is too long. Fixes issue 96.
+* Add a `keyring`-based `Storage`.
+* More robust picking up JSON error responses.
+* Make batch errors align with normal errors.
+* Add a Google Compute sample.
+* Token refresh to work with old GData API.
+* Loading of `client_secrets` JSON file backed by a cache.
+* Switch to new discovery path parameters.
+* Add support for `additionalProperties` when printing schema'd objects.
+* [Fix media upload parameter names.](http://codereview.appspot.com/6374062/)
+* oauth2client support for URL-encoded format of exchange token response (e.g.
+  Facebook)
+* Build cleaner and easier to read docs for dynamic surfaces.
+
+## v1.0c2
+
+* Parameter values of None should be treated as missing. Fixes issue 144.
+* Distribute the samples separately from the library source. Fixes issue 155.
+* Move all remaining samples over to `client_secrets.json`. Fixes issue 156.
+* Make `locked_file.py` understand win32file primitives for better
+  awesomeness.
+
+## v1.0c1
+
+* Documentation for the library has
+  [switched to epydoc](http://google-api-python-client.googlecode.com/hg/docs/epy/index.html)
+* Many improvements for media support:
+  + Added media download support, including resumable downloads.
+  + Better handling of streams that report their size as 0.
+  + Update `MediaUpload` to include `io.Base` and also fix some bugs.
+* OAuth bug fixes and improvements.
+  + Remove OAuth 1.0 support.
+  + Added `credentials_from_code` and `credentials_from_clientsecrets_and_code`.
+  + Make oauth2client support Windows-friendly locking.
+  + Fix bug in `StorageByKeyName`.
+  + Fix `None` handling in Django fields.
+    [Fixes issue 128](http://codereview.appspot.com/6298084/).
+* [Add epydoc generated docs.](http://codereview.appspot.com/6305043/)
+* Move to PEP386 compliant version numbers.
+* New and updated samples
+  + Ad Exchange Buyer API v1 code samples.
+  + Automatically generate Samples wiki page from `README` files.
+  + Update Google Prediction samples.
+  + Add a Tasks sample that demonstrates Service accounts.
+  + [new analytics api samples.](http://codereview.appspot.com/5494058/)
+* Convert all inline samples to the Farm API for consistency.
+
+## v1.0beta8
+
+* Updated media upload support.
+* Many fixes for batch requests.
+* Better handling for requests that don't require a body.
+* Fix issues with Google App Engine Python 2.7 runtime.
+* Better support for proxies.
+* All Storages now have a `.delete()` method.
+* Important changes which might break your code:
+  + `apiclient.anyjson` has moved to `oauth2client.anyjson`.
+  + Some calls, for example, `taskqueue().lease()` used to require a parameter
+    named body. In this new release only methods that really need to send a
+    body require a body parameter, and so you may get errors about an unknown
+    `body` parameter in your call. The solution is to remove the unneeded
+    `body={}` parameter.
+
+## v1.0beta7
+
+* Support for
+  [batch requests](http://code.google.com/p/google-api-python-client/wiki/Batch).
+* Support for
+  [media upload](http://code.google.com/p/google-api-python-client/wiki/MediaUpload).
+* Better handling for APIs that return something other than JSON.
+* Major cleanup and consolidation of the samples.
+* Bug fixes and other enhancements:
+   72  Defect  Appengine OAuth2Decorator: Convert redirect address to string
+   22  Defect  Better error handling for unknown service name or version
+   48  Defect  StorageByKeyName().get() has side effects
+   50  Defect  Need sample client code for Admin Audit API
+   28  Defect  better comments for app engine sample   Nov 9
+   63  Enhancement Let OAuth2Decorator take a list of scope
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..15b9455
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,208 @@
+Contributing
+============
+
+1.  **Please sign one of the contributor license agreements [below][6].**
+1.  [File an issue][9] to notify the maintainers about what you're working on.
+1.  [Fork the repo][10], develop and [test][11] your code changes, add docs.
+1.  Make sure that your commit messages clearly describe the changes.
+1.  [Send][12] a pull request.
+
+Here are some guidelines for hacking on `oauth2client`.
+
+Before writing code, file an issue
+----------------------------------
+
+Use the [issue tracker][7] to start the discussion. It is possible that someone
+else is already working on your idea, your approach is not quite right, or that
+the functionality exists already. The ticket you file in the issue tracker will
+be used to hash that all out.
+
+Fork `oauth2client`
+-------------------
+
+We will use GitHub's mechanism for [forking][8] repositories and making pull
+requests. Fork the repository, and make your changes in the forked repository.
+
+Include tests
+-------------
+
+Be sure to add the relevant tests before making the pull request. Docs will be
+updated automatically when we merge to `master`, but you should also build
+the docs yourself via `tox -e docs` and make sure they're readable.
+
+Make the pull request
+---------------------
+
+Once you have made all your changes, tests, and updated the documentation,
+make a pull request to move everything back into the main `oauth2client`
+repository. Be sure to reference the original issue in the pull request.
+Expect some back-and-forth with regards to style and compliance of these
+rules. In particular:
+* `oauth2client` follows the [Google Python Style Guide][GooglePythonStyle].
+* Follow [these guidelines][GitCommitRules] when authoring your commit message.
+
+Using a Development Checkout
+----------------------------
+
+You’ll have to create a development environment to hack on
+`oauth2client`, using a Git checkout:
+
+-   While logged into your GitHub account, navigate to the `oauth2client`
+    [repo][1] on GitHub.
+-   Fork and clone the `oauth2client` repository to your GitHub account
+    by clicking the "Fork" button.
+-   Clone your fork of `oauth2client` from your GitHub account to your
+    local computer, substituting your account username and specifying
+    the destination as `hack-on-oauth2client`. For example:
+
+    ```bash
+    $ cd ${HOME}
+    $ git clone git@github.com:USERNAME/oauth2client.git hack-on-oauth2client
+    $ cd hack-on-oauth2client
+    $ # Configure remotes such that you can pull changes from the oauth2client
+    $ # repository into your local repository.
+    $ git remote add upstream https://github.com:google/oauth2client
+    $ # fetch and merge changes from upstream into master
+    $ git fetch upstream
+    $ git merge upstream/master
+    ```
+
+Now your local repo is set up such that you will push changes to your
+GitHub repo, from which you can submit a pull request.
+
+-   Create a virtualenv in which to install `oauth2client`:
+
+    ```bash
+    $ cd ~/hack-on-oauth2client
+    $ virtualenv -ppython2.7 env
+    ```
+
+    Note that very old versions of virtualenv (virtualenv versions
+    below, say, 1.10 or thereabouts) require you to pass a
+    `--no-site-packages` flag to get a completely isolated environment.
+
+    You can choose which Python version you want to use by passing a
+    `-p` flag to `virtualenv`. For example, `virtualenv -ppython2.7`
+    chooses the Python 2.7 interpreter to be installed.
+
+    From here on in within these instructions, the
+    `~/hack-on-oauth2client/env` virtual environment you created above will be
+    referred to as `$VENV`. To use the instructions in the steps that
+    follow literally, use the `export VENV=~/hack-on-oauth2client/env`
+    command.
+
+-   Install `oauth2client` from the checkout into the virtualenv using
+    `setup.py develop`. Running `setup.py develop` **must** be done while
+    the current working directory is the `oauth2client` checkout
+    directory:
+
+    ```bash
+    $ cd ~/hack-on-oauth2client
+    $ $VENV/bin/python setup.py develop
+    ```
+
+Running Tests
+--------------
+
+-   To run all tests for `oauth2client` on a single Python version, run
+    `nosetests` from your development virtualenv (See
+    **Using a Development Checkout** above).
+
+-   To run the full set of `oauth2client` tests on all platforms, install
+    [`tox`][2] into a system Python.  The `tox` console script will be
+    installed into the scripts location for that Python.  While in the
+    `oauth2client` checkout root directory (it contains `tox.ini`),
+    invoke the `tox` console script.  This will read the `tox.ini` file and
+    execute the tests on multiple Python versions and platforms; while it runs,
+    it creates a virtualenv for each version/platform combination.  For
+    example:
+
+    ```bash
+    $ sudo pip install tox
+    $ cd ~/hack-on-oauth2client
+    $ tox
+    ```
+
+-   In order to run the `pypy` environment (in `tox`) you'll need at
+    least version 2.6 of `pypy` installed. See the [docs][13] for
+    more information.
+
+-   **Note** that `django` related tests are turned off for Python 2.6
+    and 3.3. This is because `django` dropped support for
+    [2.6 in `django==1.7`][14] and for [3.3 in `django==1.9`][15].
+
+Running System Tests
+--------------------
+
+-   To run system tests you can execute:
+
+    ```bash
+    $ tox -e system-tests
+    $ tox -e system-tests3
+    ```
+
+    This alone will not run the tests. You'll need to change some local
+    auth settings and download some service account configuration files
+    from your project to run all the tests.
+
+-   System tests will be run against an actual project and so you'll need to
+    provide some environment variables to facilitate this.
+
+    -   `OAUTH2CLIENT_TEST_JSON_KEY_PATH`: The path to a service account JSON
+        key file; see `tests/data/gcloud/application_default_credentials.json`
+        as an example. Such a file can be downloaded directly from the
+        developer's console by clicking "Generate new JSON key". See private
+        key [docs][3] for more details.
+    -   `OAUTH2CLIENT_TEST_P12_KEY_PATH`: The path to a service account
+        P12/PKCS12 key file. You can download this in the same way as a JSON
+        key, just select "P12 Key" as your "Key type" when downloading.
+    -   `OAUTH2CLIENT_TEST_P12_KEY_EMAIL`: The service account email
+        corresponding to the P12/PKCS12 key file.
+    -   `OAUTH2CLIENT_TEST_USER_KEY_PATH`: The path to a JSON key file for a
+        user. If this is not set, the file created by running
+        `gcloud auth login` will be used. See
+        `tests/data/gcloud/application_default_credentials_authorized_user.json`
+        for an example.
+    -   `OAUTH2CLIENT_TEST_USER_KEY_EMAIL`: The user account email
+        corresponding to the user JSON key file.
+
+-   Examples of these can be found in `scripts/local_test_setup.sample`. We
+    recommend copying this to `scripts/local_test_setup`, editing the values
+    and sourcing them into your environment:
+
+    ```bash
+    $ source scripts/local_test_setup
+    ```
+
+Contributor License Agreements
+------------------------------
+
+Before we can accept your pull requests you'll need to sign a Contributor
+License Agreement (CLA):
+
+-   **If you are an individual writing original source code** and **you own
+    the intellectual property**, then you'll need to sign an
+    [individual CLA][4].
+-   **If you work for a company that wants to allow you to contribute your
+    work**, then you'll need to sign a [corporate CLA][5].
+
+You can sign these electronically (just scroll to the bottom). After that,
+we'll be able to accept your pull requests.
+
+[1]: https://github.com/google/oauth2client
+[2]: https://tox.readthedocs.io/en/latest/
+[3]: https://cloud.google.com/storage/docs/authentication#generating-a-private-key
+[4]: https://developers.google.com/open-source/cla/individual
+[5]: https://developers.google.com/open-source/cla/corporate
+[6]: #contributor-license-agreements
+[7]: https://github.com/google/oauth2client/issues
+[8]: https://help.github.com/articles/fork-a-repo/
+[9]: #before-writing-code-file-an-issue
+[10]: #fork-oauth2client
+[11]: #include-tests
+[12]: #make-the-pull-request
+[13]: https://oauth2client.readthedocs.io/en/latest/#using-pypy
+[14]: https://docs.djangoproject.com/en/1.7/faq/install/#what-python-version-can-i-use-with-django
+[15]: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django
+[GooglePythonStyle]: https://google.github.io/styleguide/pyguide.html
+[GitCommitRules]: http://chris.beams.io/posts/git-commit/#seven-rules
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b506d50
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+ Copyright 2014 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.
+
+Dependent Modules
+=================
+
+This code has the following dependencies
+above and beyond the Python standard library:
+
+uritemplates - Apache License 2.0
+httplib2 - MIT License
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..39f5637
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.md
+recursive-exclude tests *
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..6a258aa
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,16 @@
+name: "oauth2client"
+description:
+    "This is a client library for accessing resources protected by OAuth 2.0."
+
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://pypi.org/project/oauth2client"
+  }
+  url {
+    type: GIT
+    value: "https://github.com/google/oauth2client"
+  }
+  version: "v3.0.0"
+  last_upgrade_date { year: 2018 month: 6 day: 6 }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d0141e3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+test:
+	tox
+
+docs:
+	scripts/doc-build
diff --git a/NOTICE b/NOTICE
new file mode 120000
index 0000000..7a694c9
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+LICENSE
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17e69fc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+[![Build Status](https://travis-ci.org/google/oauth2client.svg?branch=master)](https://travis-ci.org/google/oauth2client)
+[![Coverage Status](https://coveralls.io/repos/google/oauth2client/badge.svg?branch=master&service=github)](https://coveralls.io/github/google/oauth2client?branch=master)
+[![Documentation Status](https://readthedocs.org/projects/oauth2client/badge/?version=latest)](https://oauth2client.readthedocs.io/)
+
+This is a client library for accessing resources protected by OAuth 2.0.
+
+Installation
+============
+
+To install, simply run the following command in your terminal:
+
+```bash
+$ pip install --upgrade oauth2client
+```
+
+Contributing
+============
+
+Please see the [CONTRIBUTING page][1] for more information. In particular, we
+love pull requests -- but please make sure to sign the contributor license
+agreement.
+
+Supported Python Versions
+=========================
+
+We support Python 2.6, 2.7, 3.3+. More information [in the docs][2].
+
+[1]: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md
+[2]: https://oauth2client.readthedocs.io/#supported-python-versions
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..6ed721d
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    = -W
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  xml        to make Docutils-native XML files"
+	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/oauth2client.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/oauth2client.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/oauth2client"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/oauth2client"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through platex and dvipdfmx..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+	@echo
+	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+	@echo
+	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico
new file mode 100644
index 0000000..c040e7f
--- /dev/null
+++ b/docs/_static/favicon.ico
Binary files differ
diff --git a/docs/_static/google_logo.png b/docs/_static/google_logo.png
new file mode 100644
index 0000000..2cf8fe6
--- /dev/null
+++ b/docs/_static/google_logo.png
Binary files differ
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..1b4663b
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+#
+# oauth2client documentation build configuration file, created by
+# sphinx-quickstart on Wed Dec 17 23:13:19 2014.
+#
+
+import os
+import sys
+
+
+# In order to load django before 1.7, we need to create a faux
+# settings module and load it. This assumes django has been installed
+# (but it must be for the docs to build), so if it has not already
+# been installed run `pip install -r docs/requirements.txt`.
+os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.contrib.django_util.settings'
+import django
+import mock
+from pkg_resources import get_distribution
+if django.VERSION[1] < 7:
+    sys.path.insert(0, '.')
+
+# See https://read-the-docs.readthedocs.io/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules
+
+
+class Mock(mock.Mock):
+
+    @classmethod
+    def __getattr__(cls, name):
+            return Mock()
+
+
+MOCK_MODULES = (
+    'google',
+    'google.appengine',
+    'google.appengine.api',
+    'google.appengine.api.app_identiy',
+    'google.appengine.api.urlfetch',
+    'google.appengine.ext',
+    'google.appengine.ext.webapp',
+    'google.appengine.ext.webapp.util',
+    'werkzeug.local',
+)
+
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('..'))
+
+# -- General configuration ------------------------------------------------
+
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.coverage',
+    'sphinx.ext.napoleon',
+    'sphinx.ext.viewcode',
+]
+templates_path = ['_templates']
+source_suffix = '.rst'
+master_doc = 'index'
+
+# General information about the project.
+project = u'oauth2client'
+copyright = u'2014, Google, Inc'
+
+# Version info
+distro = get_distribution('oauth2client')
+version = distro.version
+release = distro.version
+
+exclude_patterns = ['_build']
+
+# -- Options for HTML output ----------------------------------------------
+
+# We fake our more expensive imports when building the docs.
+sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
+
+# We want to set the RTD theme, but not if we're on RTD.
+if os.environ.get('READTHEDOCS', None) != 'True':
+    import sphinx_rtd_theme
+    html_theme = 'sphinx_rtd_theme'
+    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+html_favicon = '_static/favicon.ico'
+
+html_static_path = ['_static']
+html_logo = '_static/google_logo.png'
+htmlhelp_basename = 'oauth2clientdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {}
+latex_documents = [
+    ('index', 'oauth2client.tex', u'oauth2client Documentation',
+     u'Google, Inc.', 'manual'),
+]
+
+# -- Options for manual page output ---------------------------------------
+
+man_pages = [
+    ('index', 'oauth2client', u'oauth2client Documentation',
+     [u'Google, Inc.'], 1)
+]
+
+# -- Options for Texinfo output -------------------------------------------
+
+texinfo_documents = [
+    ('index', 'oauth2client', u'oauth2client Documentation',
+     u'Google, Inc.', 'oauth2client', 'One line description of project.',
+     'Miscellaneous'),
+]
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..0543e1a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,124 @@
+oauth2client
+============
+
+*making OAuth2 just a little less painful*
+
+``oauth2client`` makes it easy to interact with OAuth2-protected resources,
+especially those related to Google APIs. You can also start with `general
+information about using OAuth2 with Google APIs
+<https://developers.google.com/accounts/docs/OAuth2>`_.
+
+Getting started
+---------------
+
+We recommend installing via ``pip``:
+
+.. code-block:: bash
+
+    $ pip install --upgrade oauth2client
+
+You can also install from source:
+
+.. code-block:: bash
+
+    $ git clone https://github.com/google/oauth2client
+    $ cd oauth2client
+    $ python setup.py install
+
+Using ``pypy``
+--------------
+
+-   In order to use crypto libraries (e.g. for service accounts) you will
+    need to install one of ``pycrypto`` or ``pyOpenSSL``.
+-   Using ``pycrypto`` with ``pypy`` will be in general problematic. If
+    ``libgmp`` is installed on your machine, the ``pycrypto`` install will
+    attempt to build ``_fastmath.c``. However, this file uses CPython
+    implementation details and hence can't be built in ``pypy`` (as of
+    ``pypy`` 2.6 and ``pycrypto`` 2.6.1). In order to install
+
+    .. code-block:: bash
+
+        with_gmp=no pip install --upgrade pycrypto
+
+    See discussions on the `pypy issue tracker`_ and the
+    `pycrypto issue tracker`_.
+
+-   Using ``pyOpenSSL`` with versions of ``pypy`` before 2.6 may be in general
+    problematic since ``pyOpenSSL`` depends on the ``cryptography`` library.
+    For versions of ``cryptography`` before 1.0, importing ``pyOpenSSL``
+    with it caused `massive startup costs`_. In order to address this
+    slow startup, ``cryptography`` 1.0 made some `changes`_ in how it used
+    ``cffi`` when means it can't be used on versions of ``pypy`` before 2.6.
+
+    The default version of ``pypy`` you get when installed
+
+    .. code-block:: bash
+
+        apt-get install pypy pypy-dev
+
+    on `Ubuntu 14.04`_ is 2.2.1. In order to upgrade, you'll need to use
+    the `pypy/ppa PPA`_:
+
+    .. code-block:: bash
+
+        apt-get purge pypy pypy-dev
+        add-apt-repository ppa:pypy/ppa
+        apt-get update
+        apt-get install pypy pypy-dev
+
+.. _pypy issue tracker: https://bitbucket.org/pypy/pypy/issues/997
+.. _pycrypto issue tracker: https://github.com/dlitz/pycrypto/pull/59
+.. _massive startup costs: https://github.com/pyca/pyopenssl/issues/137
+.. _changes: https://github.com/pyca/cryptography/issues/2275#issuecomment-130751514
+.. _Ubuntu 14.04: http://packages.ubuntu.com/trusty/pypy
+.. _pypy/ppa PPA: https://launchpad.net/~pypy/+archive/ubuntu/ppa
+
+Downloads
+^^^^^^^^^
+
+* `Most recent release tarball
+  <https://github.com/google/oauth2client/tarball/master>`_
+* `Most recent release zipfile
+  <https://github.com/google/oauth2client/zipball/master>`_
+* `Complete release list <https://github.com/google/oauth2client/releases>`_
+
+Library Documentation
+---------------------
+
+* Complete library index: :ref:`genindex`
+* Index of all modules: :ref:`modindex`
+* Search all documentation: :ref:`search`
+
+Contributing
+------------
+
+Please see the `contributing page`_ for more information.
+In particular, we love pull requests -- but please make sure to sign the
+contributor license agreement.
+
+.. _contributing page: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md
+
+.. toctree::
+   :maxdepth: 1
+   :hidden:
+
+   source/oauth2client
+
+Supported Python Versions
+-------------------------
+
+We support Python 2.6, 2.7, 3.3+. (Whatever this file says, the truth is
+always represented by our `tox.ini`_).
+
+.. _tox.ini: https://github.com/google/oauth2client/blob/master/tox.ini
+
+We explicitly decided to support Python 3 beginning with version
+3.3. Reasons for this include:
+
+* Encouraging use of newest versions of Python 3
+* Following the lead of prominent `open-source projects`_
+* Unicode literal support which
+  allows for a cleaner codebase that works in both Python 2 and Python 3
+
+.. _open-source projects: http://docs.python-requests.org/en/latest/
+.. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..433a833
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,10 @@
+django
+flask
+keyring
+mock
+pycrypto>=2.6
+pyopenssl>=0.14
+python-gflags
+pyyaml
+webapp2
+WebOb
diff --git a/docs/source/oauth2client.client.rst b/docs/source/oauth2client.client.rst
new file mode 100644
index 0000000..f3b1832
--- /dev/null
+++ b/docs/source/oauth2client.client.rst
@@ -0,0 +1,7 @@
+oauth2client.client module
+==========================
+
+.. automodule:: oauth2client.client
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.clientsecrets.rst b/docs/source/oauth2client.clientsecrets.rst
new file mode 100644
index 0000000..d666564
--- /dev/null
+++ b/docs/source/oauth2client.clientsecrets.rst
@@ -0,0 +1,7 @@
+oauth2client.clientsecrets module
+=================================
+
+.. automodule:: oauth2client.clientsecrets
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.appengine.rst b/docs/source/oauth2client.contrib.appengine.rst
new file mode 100644
index 0000000..7f3d5e2
--- /dev/null
+++ b/docs/source/oauth2client.contrib.appengine.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.appengine module
+=====================================
+
+.. automodule:: oauth2client.contrib.appengine
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.devshell.rst b/docs/source/oauth2client.contrib.devshell.rst
new file mode 100644
index 0000000..20d5c41
--- /dev/null
+++ b/docs/source/oauth2client.contrib.devshell.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.devshell module
+====================================
+
+.. automodule:: oauth2client.contrib.devshell
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.dictionary_storage.rst b/docs/source/oauth2client.contrib.dictionary_storage.rst
new file mode 100644
index 0000000..1b59a2c
--- /dev/null
+++ b/docs/source/oauth2client.contrib.dictionary_storage.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.dictionary_storage module
+==============================================
+
+.. automodule:: oauth2client.contrib.dictionary_storage
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.apps.rst b/docs/source/oauth2client.contrib.django_util.apps.rst
new file mode 100644
index 0000000..b7c91ae
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.apps.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.apps module
+============================================
+
+.. automodule:: oauth2client.contrib.django_util.apps
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.decorators.rst b/docs/source/oauth2client.contrib.django_util.decorators.rst
new file mode 100644
index 0000000..07350bc
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.decorators.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.decorators module
+==================================================
+
+.. automodule:: oauth2client.contrib.django_util.decorators
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.models.rst b/docs/source/oauth2client.contrib.django_util.models.rst
new file mode 100644
index 0000000..4be59d3
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.models.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.models module
+==============================================
+
+.. automodule:: oauth2client.contrib.django_util.models
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.rst b/docs/source/oauth2client.contrib.django_util.rst
new file mode 100644
index 0000000..f60195a
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.rst
@@ -0,0 +1,23 @@
+oauth2client.contrib.django_util package
+========================================
+
+Submodules
+----------
+
+.. toctree::
+
+   oauth2client.contrib.django_util.apps
+   oauth2client.contrib.django_util.decorators
+   oauth2client.contrib.django_util.models
+   oauth2client.contrib.django_util.signals
+   oauth2client.contrib.django_util.site
+   oauth2client.contrib.django_util.storage
+   oauth2client.contrib.django_util.views
+
+Module contents
+---------------
+
+.. automodule:: oauth2client.contrib.django_util
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.signals.rst b/docs/source/oauth2client.contrib.django_util.signals.rst
new file mode 100644
index 0000000..70b5d2d
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.signals.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.signals module
+===============================================
+
+.. automodule:: oauth2client.contrib.django_util.signals
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.site.rst b/docs/source/oauth2client.contrib.django_util.site.rst
new file mode 100644
index 0000000..a271b98
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.site.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.site module
+============================================
+
+.. automodule:: oauth2client.contrib.django_util.site
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.storage.rst b/docs/source/oauth2client.contrib.django_util.storage.rst
new file mode 100644
index 0000000..393e738
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.storage.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.storage module
+===============================================
+
+.. automodule:: oauth2client.contrib.django_util.storage
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.django_util.views.rst b/docs/source/oauth2client.contrib.django_util.views.rst
new file mode 100644
index 0000000..4cbbea0
--- /dev/null
+++ b/docs/source/oauth2client.contrib.django_util.views.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.django_util.views module
+=============================================
+
+.. automodule:: oauth2client.contrib.django_util.views
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.flask_util.rst b/docs/source/oauth2client.contrib.flask_util.rst
new file mode 100644
index 0000000..8ff2355
--- /dev/null
+++ b/docs/source/oauth2client.contrib.flask_util.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.flask_util module
+======================================
+
+.. automodule:: oauth2client.contrib.flask_util
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.gce.rst b/docs/source/oauth2client.contrib.gce.rst
new file mode 100644
index 0000000..a3748b6
--- /dev/null
+++ b/docs/source/oauth2client.contrib.gce.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.gce module
+===============================
+
+.. automodule:: oauth2client.contrib.gce
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.keyring_storage.rst b/docs/source/oauth2client.contrib.keyring_storage.rst
new file mode 100644
index 0000000..0fd7476
--- /dev/null
+++ b/docs/source/oauth2client.contrib.keyring_storage.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.keyring_storage module
+===========================================
+
+.. automodule:: oauth2client.contrib.keyring_storage
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.locked_file.rst b/docs/source/oauth2client.contrib.locked_file.rst
new file mode 100644
index 0000000..1076e29
--- /dev/null
+++ b/docs/source/oauth2client.contrib.locked_file.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.locked_file module
+=======================================
+
+.. automodule:: oauth2client.contrib.locked_file
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.multiprocess_file_storage.rst b/docs/source/oauth2client.contrib.multiprocess_file_storage.rst
new file mode 100644
index 0000000..6f683a0
--- /dev/null
+++ b/docs/source/oauth2client.contrib.multiprocess_file_storage.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.multiprocess_file_storage module
+=====================================================
+
+.. automodule:: oauth2client.contrib.multiprocess_file_storage
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.multistore_file.rst b/docs/source/oauth2client.contrib.multistore_file.rst
new file mode 100644
index 0000000..2787b10
--- /dev/null
+++ b/docs/source/oauth2client.contrib.multistore_file.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.multistore_file module
+===========================================
+
+.. automodule:: oauth2client.contrib.multistore_file
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.rst b/docs/source/oauth2client.contrib.rst
new file mode 100644
index 0000000..44be6f9
--- /dev/null
+++ b/docs/source/oauth2client.contrib.rst
@@ -0,0 +1,34 @@
+oauth2client.contrib package
+============================
+
+Subpackages
+-----------
+
+.. toctree::
+
+    oauth2client.contrib.django_util
+
+Submodules
+----------
+
+.. toctree::
+
+   oauth2client.contrib.appengine
+   oauth2client.contrib.devshell
+   oauth2client.contrib.dictionary_storage
+   oauth2client.contrib.flask_util
+   oauth2client.contrib.gce
+   oauth2client.contrib.keyring_storage
+   oauth2client.contrib.locked_file
+   oauth2client.contrib.multiprocess_file_storage
+   oauth2client.contrib.multistore_file
+   oauth2client.contrib.sqlalchemy
+   oauth2client.contrib.xsrfutil
+
+Module contents
+---------------
+
+.. automodule:: oauth2client.contrib
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.sqlalchemy.rst b/docs/source/oauth2client.contrib.sqlalchemy.rst
new file mode 100644
index 0000000..94eeeec
--- /dev/null
+++ b/docs/source/oauth2client.contrib.sqlalchemy.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.sqlalchemy module
+======================================
+
+.. automodule:: oauth2client.contrib.sqlalchemy
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.xsrfutil.rst b/docs/source/oauth2client.contrib.xsrfutil.rst
new file mode 100644
index 0000000..dd5e8d6
--- /dev/null
+++ b/docs/source/oauth2client.contrib.xsrfutil.rst
@@ -0,0 +1,7 @@
+oauth2client.contrib.xsrfutil module
+====================================
+
+.. automodule:: oauth2client.contrib.xsrfutil
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.crypt.rst b/docs/source/oauth2client.crypt.rst
new file mode 100644
index 0000000..c3b6acc
--- /dev/null
+++ b/docs/source/oauth2client.crypt.rst
@@ -0,0 +1,7 @@
+oauth2client.crypt module
+=========================
+
+.. automodule:: oauth2client.crypt
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.file.rst b/docs/source/oauth2client.file.rst
new file mode 100644
index 0000000..52a9e94
--- /dev/null
+++ b/docs/source/oauth2client.file.rst
@@ -0,0 +1,7 @@
+oauth2client.file module
+========================
+
+.. automodule:: oauth2client.file
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.rst b/docs/source/oauth2client.rst
new file mode 100644
index 0000000..65de8ac
--- /dev/null
+++ b/docs/source/oauth2client.rst
@@ -0,0 +1,31 @@
+oauth2client package
+====================
+
+Subpackages
+-----------
+
+.. toctree::
+
+    oauth2client.contrib
+
+Submodules
+----------
+
+.. toctree::
+
+   oauth2client.client
+   oauth2client.clientsecrets
+   oauth2client.crypt
+   oauth2client.file
+   oauth2client.service_account
+   oauth2client.tools
+   oauth2client.transport
+   oauth2client.util
+
+Module contents
+---------------
+
+.. automodule:: oauth2client
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.service_account.rst b/docs/source/oauth2client.service_account.rst
new file mode 100644
index 0000000..0d3b382
--- /dev/null
+++ b/docs/source/oauth2client.service_account.rst
@@ -0,0 +1,7 @@
+oauth2client.service_account module
+===================================
+
+.. automodule:: oauth2client.service_account
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.tools.rst b/docs/source/oauth2client.tools.rst
new file mode 100644
index 0000000..240ad52
--- /dev/null
+++ b/docs/source/oauth2client.tools.rst
@@ -0,0 +1,7 @@
+oauth2client.tools module
+=========================
+
+.. automodule:: oauth2client.tools
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.transport.rst b/docs/source/oauth2client.transport.rst
new file mode 100644
index 0000000..1c6dbb0
--- /dev/null
+++ b/docs/source/oauth2client.transport.rst
@@ -0,0 +1,7 @@
+oauth2client.transport module
+=============================
+
+.. automodule:: oauth2client.transport
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/oauth2client.util.rst b/docs/source/oauth2client.util.rst
new file mode 100644
index 0000000..21dc8c8
--- /dev/null
+++ b/docs/source/oauth2client.util.rst
@@ -0,0 +1,7 @@
+oauth2client.util module
+========================
+
+.. automodule:: oauth2client.util
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/oauth2client/Android.bp b/oauth2client/Android.bp
new file mode 100644
index 0000000..7818920
--- /dev/null
+++ b/oauth2client/Android.bp
@@ -0,0 +1,38 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// 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.
+python_library {
+    name: "py-oauth2client",
+    host_supported: true,
+    srcs: [
+        "*.py",
+        "contrib/*.py",
+        "contrib/django_util/*.py",
+    ],
+    version: {
+        py2: {
+            enabled: true,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+    libs: [
+        "py-httplib2",
+        "py-pyasn1",
+        "py-pyasn1-modules",
+        "py-rsa",
+        "py-six",
+    ],
+    pkg_path: "oauth2client",
+}
diff --git a/oauth2client/__init__.py b/oauth2client/__init__.py
new file mode 100644
index 0000000..28384bb
--- /dev/null
+++ b/oauth2client/__init__.py
@@ -0,0 +1,23 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Client library for using OAuth2, especially with Google APIs."""
+
+__version__ = '3.0.0'
+
+GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
+GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
+GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
+GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
+GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
diff --git a/oauth2client/_helpers.py b/oauth2client/_helpers.py
new file mode 100644
index 0000000..cb959c5
--- /dev/null
+++ b/oauth2client/_helpers.py
@@ -0,0 +1,105 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+"""Helper functions for commonly used utilities."""
+
+import base64
+import json
+
+import six
+
+
+def _parse_pem_key(raw_key_input):
+    """Identify and extract PEM keys.
+
+    Determines whether the given key is in the format of PEM key, and extracts
+    the relevant part of the key if it is.
+
+    Args:
+        raw_key_input: The contents of a private key file (either PEM or
+                       PKCS12).
+
+    Returns:
+        string, The actual key if the contents are from a PEM file, or
+        else None.
+    """
+    offset = raw_key_input.find(b'-----BEGIN ')
+    if offset != -1:
+        return raw_key_input[offset:]
+
+
+def _json_encode(data):
+    return json.dumps(data, separators=(',', ':'))
+
+
+def _to_bytes(value, encoding='ascii'):
+    """Converts a string value to bytes, if necessary.
+
+    Unfortunately, ``six.b`` is insufficient for this task since in
+    Python2 it does not modify ``unicode`` objects.
+
+    Args:
+        value: The string/bytes value to be converted.
+        encoding: The encoding to use to convert unicode to bytes. Defaults
+                  to "ascii", which will not allow any characters from ordinals
+                  larger than 127. Other useful values are "latin-1", which
+                  which will only allows byte ordinals (up to 255) and "utf-8",
+                  which will encode any unicode that needs to be.
+
+    Returns:
+        The original value converted to bytes (if unicode) or as passed in
+        if it started out as bytes.
+
+    Raises:
+        ValueError if the value could not be converted to bytes.
+    """
+    result = (value.encode(encoding)
+              if isinstance(value, six.text_type) else value)
+    if isinstance(result, six.binary_type):
+        return result
+    else:
+        raise ValueError('{0!r} could not be converted to bytes'.format(value))
+
+
+def _from_bytes(value):
+    """Converts bytes to a string value, if necessary.
+
+    Args:
+        value: The string/bytes value to be converted.
+
+    Returns:
+        The original value converted to unicode (if bytes) or as passed in
+        if it started out as unicode.
+
+    Raises:
+        ValueError if the value could not be converted to unicode.
+    """
+    result = (value.decode('utf-8')
+              if isinstance(value, six.binary_type) else value)
+    if isinstance(result, six.text_type):
+        return result
+    else:
+        raise ValueError(
+            '{0!r} could not be converted to unicode'.format(value))
+
+
+def _urlsafe_b64encode(raw_bytes):
+    raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
+    return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
+
+
+def _urlsafe_b64decode(b64string):
+    # Guard against unicode strings, which base64 can't handle.
+    b64string = _to_bytes(b64string)
+    padded = b64string + b'=' * (4 - len(b64string) % 4)
+    return base64.urlsafe_b64decode(padded)
diff --git a/oauth2client/_openssl_crypt.py b/oauth2client/_openssl_crypt.py
new file mode 100644
index 0000000..77fac74
--- /dev/null
+++ b/oauth2client/_openssl_crypt.py
@@ -0,0 +1,136 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+"""OpenSSL Crypto-related routines for oauth2client."""
+
+from OpenSSL import crypto
+
+from oauth2client import _helpers
+
+
+class OpenSSLVerifier(object):
+    """Verifies the signature on a message."""
+
+    def __init__(self, pubkey):
+        """Constructor.
+
+        Args:
+            pubkey: OpenSSL.crypto.PKey, The public key to verify with.
+        """
+        self._pubkey = pubkey
+
+    def verify(self, message, signature):
+        """Verifies a message against a signature.
+
+        Args:
+        message: string or bytes, The message to verify. If string, will be
+                 encoded to bytes as utf-8.
+        signature: string or bytes, The signature on the message. If string,
+                   will be encoded to bytes as utf-8.
+
+        Returns:
+            True if message was signed by the private key associated with the
+            public key that this object was constructed with.
+        """
+        message = _helpers._to_bytes(message, encoding='utf-8')
+        signature = _helpers._to_bytes(signature, encoding='utf-8')
+        try:
+            crypto.verify(self._pubkey, signature, message, 'sha256')
+            return True
+        except crypto.Error:
+            return False
+
+    @staticmethod
+    def from_string(key_pem, is_x509_cert):
+        """Construct a Verified instance from a string.
+
+        Args:
+            key_pem: string, public key in PEM format.
+            is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
+                          is expected to be an RSA key in PEM format.
+
+        Returns:
+            Verifier instance.
+
+        Raises:
+            OpenSSL.crypto.Error: if the key_pem can't be parsed.
+        """
+        key_pem = _helpers._to_bytes(key_pem)
+        if is_x509_cert:
+            pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
+        else:
+            pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
+        return OpenSSLVerifier(pubkey)
+
+
+class OpenSSLSigner(object):
+    """Signs messages with a private key."""
+
+    def __init__(self, pkey):
+        """Constructor.
+
+        Args:
+            pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
+        """
+        self._key = pkey
+
+    def sign(self, message):
+        """Signs a message.
+
+        Args:
+            message: bytes, Message to be signed.
+
+        Returns:
+            string, The signature of the message for the given key.
+        """
+        message = _helpers._to_bytes(message, encoding='utf-8')
+        return crypto.sign(self._key, message, 'sha256')
+
+    @staticmethod
+    def from_string(key, password=b'notasecret'):
+        """Construct a Signer instance from a string.
+
+        Args:
+            key: string, private key in PKCS12 or PEM format.
+            password: string, password for the private key file.
+
+        Returns:
+            Signer instance.
+
+        Raises:
+            OpenSSL.crypto.Error if the key can't be parsed.
+        """
+        key = _helpers._to_bytes(key)
+        parsed_pem_key = _helpers._parse_pem_key(key)
+        if parsed_pem_key:
+            pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
+        else:
+            password = _helpers._to_bytes(password, encoding='utf-8')
+            pkey = crypto.load_pkcs12(key, password).get_privatekey()
+        return OpenSSLSigner(pkey)
+
+
+def pkcs12_key_as_pem(private_key_bytes, private_key_password):
+    """Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
+
+    Args:
+        private_key_bytes: Bytes. PKCS#12 key in DER format.
+        private_key_password: String. Password for PKCS#12 key.
+
+    Returns:
+        String. PEM contents of ``private_key_bytes``.
+    """
+    private_key_password = _helpers._to_bytes(private_key_password)
+    pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
+    return crypto.dump_privatekey(crypto.FILETYPE_PEM,
+                                  pkcs12.get_privatekey())
diff --git a/oauth2client/_pure_python_crypt.py b/oauth2client/_pure_python_crypt.py
new file mode 100644
index 0000000..2c5d43a
--- /dev/null
+++ b/oauth2client/_pure_python_crypt.py
@@ -0,0 +1,184 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Pure Python crypto-related routines for oauth2client.
+
+Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
+to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
+certificates.
+"""
+
+from pyasn1.codec.der import decoder
+from pyasn1_modules import pem
+from pyasn1_modules.rfc2459 import Certificate
+from pyasn1_modules.rfc5208 import PrivateKeyInfo
+import rsa
+import six
+
+from oauth2client import _helpers
+
+
+_PKCS12_ERROR = r"""\
+PKCS12 format is not supported by the RSA library.
+Either install PyOpenSSL, or please convert .p12 format
+to .pem format:
+    $ cat key.p12 | \
+    >   openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
+    >   openssl rsa > key.pem
+"""
+
+_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
+_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
+                 '-----END RSA PRIVATE KEY-----')
+_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
+                 '-----END PRIVATE KEY-----')
+_PKCS8_SPEC = PrivateKeyInfo()
+
+
+def _bit_list_to_bytes(bit_list):
+    """Converts an iterable of 1's and 0's to bytes.
+
+    Combines the list 8 at a time, treating each group of 8 bits
+    as a single byte.
+    """
+    num_bits = len(bit_list)
+    byte_vals = bytearray()
+    for start in six.moves.xrange(0, num_bits, 8):
+        curr_bits = bit_list[start:start + 8]
+        char_val = sum(val * digit
+                       for val, digit in zip(_POW2, curr_bits))
+        byte_vals.append(char_val)
+    return bytes(byte_vals)
+
+
+class RsaVerifier(object):
+    """Verifies the signature on a message.
+
+    Args:
+        pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
+    """
+
+    def __init__(self, pubkey):
+        self._pubkey = pubkey
+
+    def verify(self, message, signature):
+        """Verifies a message against a signature.
+
+        Args:
+            message: string or bytes, The message to verify. If string, will be
+                     encoded to bytes as utf-8.
+            signature: string or bytes, The signature on the message. If
+                       string, will be encoded to bytes as utf-8.
+
+        Returns:
+            True if message was signed by the private key associated with the
+            public key that this object was constructed with.
+        """
+        message = _helpers._to_bytes(message, encoding='utf-8')
+        try:
+            return rsa.pkcs1.verify(message, signature, self._pubkey)
+        except (ValueError, rsa.pkcs1.VerificationError):
+            return False
+
+    @classmethod
+    def from_string(cls, key_pem, is_x509_cert):
+        """Construct an RsaVerifier instance from a string.
+
+        Args:
+            key_pem: string, public key in PEM format.
+            is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
+                          is expected to be an RSA key in PEM format.
+
+        Returns:
+            RsaVerifier instance.
+
+        Raises:
+            ValueError: if the key_pem can't be parsed. In either case, error
+                        will begin with 'No PEM start marker'. If
+                        ``is_x509_cert`` is True, will fail to find the
+                        "-----BEGIN CERTIFICATE-----" error, otherwise fails
+                        to find "-----BEGIN RSA PUBLIC KEY-----".
+        """
+        key_pem = _helpers._to_bytes(key_pem)
+        if is_x509_cert:
+            der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
+            asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
+            if remaining != b'':
+                raise ValueError('Unused bytes', remaining)
+
+            cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
+            key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
+            pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
+        else:
+            pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
+        return cls(pubkey)
+
+
+class RsaSigner(object):
+    """Signs messages with a private key.
+
+    Args:
+        pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
+    """
+
+    def __init__(self, pkey):
+        self._key = pkey
+
+    def sign(self, message):
+        """Signs a message.
+
+        Args:
+            message: bytes, Message to be signed.
+
+        Returns:
+            string, The signature of the message for the given key.
+        """
+        message = _helpers._to_bytes(message, encoding='utf-8')
+        return rsa.pkcs1.sign(message, self._key, 'SHA-256')
+
+    @classmethod
+    def from_string(cls, key, password='notasecret'):
+        """Construct an RsaSigner instance from a string.
+
+        Args:
+            key: string, private key in PEM format.
+            password: string, password for private key file. Unused for PEM
+                      files.
+
+        Returns:
+            RsaSigner instance.
+
+        Raises:
+            ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
+            PEM format.
+        """
+        key = _helpers._from_bytes(key)  # pem expects str in Py3
+        marker_id, key_bytes = pem.readPemBlocksFromFile(
+            six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
+
+        if marker_id == 0:
+            pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
+                                                 format='DER')
+        elif marker_id == 1:
+            key_info, remaining = decoder.decode(
+                key_bytes, asn1Spec=_PKCS8_SPEC)
+            if remaining != b'':
+                raise ValueError('Unused bytes', remaining)
+            pkey_info = key_info.getComponentByName('privateKey')
+            pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
+                                                 format='DER')
+        else:
+            raise ValueError('No key could be detected.')
+
+        return cls(pkey)
diff --git a/oauth2client/_pycrypto_crypt.py b/oauth2client/_pycrypto_crypt.py
new file mode 100644
index 0000000..fd2ce0c
--- /dev/null
+++ b/oauth2client/_pycrypto_crypt.py
@@ -0,0 +1,124 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+"""pyCrypto Crypto-related routines for oauth2client."""
+
+from Crypto.Hash import SHA256
+from Crypto.PublicKey import RSA
+from Crypto.Signature import PKCS1_v1_5
+from Crypto.Util.asn1 import DerSequence
+
+from oauth2client import _helpers
+
+
+class PyCryptoVerifier(object):
+    """Verifies the signature on a message."""
+
+    def __init__(self, pubkey):
+        """Constructor.
+
+        Args:
+            pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
+            with.
+        """
+        self._pubkey = pubkey
+
+    def verify(self, message, signature):
+        """Verifies a message against a signature.
+
+        Args:
+            message: string or bytes, The message to verify. If string, will be
+                     encoded to bytes as utf-8.
+            signature: string or bytes, The signature on the message.
+
+        Returns:
+            True if message was signed by the private key associated with the
+            public key that this object was constructed with.
+        """
+        message = _helpers._to_bytes(message, encoding='utf-8')
+        return PKCS1_v1_5.new(self._pubkey).verify(
+            SHA256.new(message), signature)
+
+    @staticmethod
+    def from_string(key_pem, is_x509_cert):
+        """Construct a Verified instance from a string.
+
+        Args:
+            key_pem: string, public key in PEM format.
+            is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
+                          is expected to be an RSA key in PEM format.
+
+        Returns:
+            Verifier instance.
+        """
+        if is_x509_cert:
+            key_pem = _helpers._to_bytes(key_pem)
+            pemLines = key_pem.replace(b' ', b'').split()
+            certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
+            certSeq = DerSequence()
+            certSeq.decode(certDer)
+            tbsSeq = DerSequence()
+            tbsSeq.decode(certSeq[0])
+            pubkey = RSA.importKey(tbsSeq[6])
+        else:
+            pubkey = RSA.importKey(key_pem)
+        return PyCryptoVerifier(pubkey)
+
+
+class PyCryptoSigner(object):
+    """Signs messages with a private key."""
+
+    def __init__(self, pkey):
+        """Constructor.
+
+        Args:
+            pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
+        """
+        self._key = pkey
+
+    def sign(self, message):
+        """Signs a message.
+
+        Args:
+            message: string, Message to be signed.
+
+        Returns:
+            string, The signature of the message for the given key.
+        """
+        message = _helpers._to_bytes(message, encoding='utf-8')
+        return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
+
+    @staticmethod
+    def from_string(key, password='notasecret'):
+        """Construct a Signer instance from a string.
+
+        Args:
+            key: string, private key in PEM format.
+            password: string, password for private key file. Unused for PEM
+                      files.
+
+        Returns:
+            Signer instance.
+
+        Raises:
+            NotImplementedError if the key isn't in PEM format.
+        """
+        parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
+        if parsed_pem_key:
+            pkey = RSA.importKey(parsed_pem_key)
+        else:
+            raise NotImplementedError(
+                'No key in PEM format was detected. This implementation '
+                'can only use the PyCrypto library for keys in PEM '
+                'format.')
+        return PyCryptoSigner(pkey)
diff --git a/oauth2client/client.py b/oauth2client/client.py
new file mode 100644
index 0000000..8956443
--- /dev/null
+++ b/oauth2client/client.py
@@ -0,0 +1,2133 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""An OAuth 2.0 client.
+
+Tools for interacting with OAuth 2.0 protected resources.
+"""
+
+import collections
+import copy
+import datetime
+import json
+import logging
+import os
+import shutil
+import socket
+import sys
+import tempfile
+
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import clientsecrets
+from oauth2client import transport
+from oauth2client import util
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+HAS_OPENSSL = False
+HAS_CRYPTO = False
+try:
+    from oauth2client import crypt
+    HAS_CRYPTO = True
+    HAS_OPENSSL = crypt.OpenSSLVerifier is not None
+except ImportError:  # pragma: NO COVER
+    pass
+
+
+logger = logging.getLogger(__name__)
+
+# Expiry is stored in RFC3339 UTC format
+EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+# Which certs to use to validate id_tokens received.
+ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
+# This symbol previously had a typo in the name; we keep the old name
+# around for now, but will remove it in the future.
+ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
+
+# Constant to use for the out of band OAuth 2.0 flow.
+OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
+
+# The value representing user credentials.
+AUTHORIZED_USER = 'authorized_user'
+
+# The value representing service account credentials.
+SERVICE_ACCOUNT = 'service_account'
+
+# The environment variable pointing the file with local
+# Application Default Credentials.
+GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
+# The ~/.config subdirectory containing gcloud credentials. Intended
+# to be swapped out in tests.
+_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
+# The environment variable name which can replace ~/.config if set.
+_CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG'
+
+# The error message we show users when we can't find the Application
+# Default Credentials.
+ADC_HELP_MSG = (
+    'The Application Default Credentials are not available. They are '
+    'available if running in Google Compute Engine. Otherwise, the '
+    'environment variable ' +
+    GOOGLE_APPLICATION_CREDENTIALS +
+    ' must be defined pointing to a file defining the credentials. See '
+    'https://developers.google.com/accounts/docs/'
+    'application-default-credentials for more information.')
+
+_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
+
+# The access token along with the seconds in which it expires.
+AccessTokenInfo = collections.namedtuple(
+    'AccessTokenInfo', ['access_token', 'expires_in'])
+
+DEFAULT_ENV_NAME = 'UNKNOWN'
+
+# If set to True _get_environment avoid GCE check (_detect_gce_environment)
+NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False')
+
+# Timeout in seconds to wait for the GCE metadata server when detecting the
+# GCE environment.
+try:
+    GCE_METADATA_TIMEOUT = int(
+        os.environ.setdefault('GCE_METADATA_TIMEOUT', '3'))
+except ValueError:  # pragma: NO COVER
+    GCE_METADATA_TIMEOUT = 3
+
+_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
+_GCE_METADATA_HOST = '169.254.169.254'
+_METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
+_DESIRED_METADATA_FLAVOR = 'Google'
+
+# Expose utcnow() at module level to allow for
+# easier testing (by replacing with a stub).
+_UTCNOW = datetime.datetime.utcnow
+
+# NOTE: These names were previously defined in this module but have been
+#       moved into `oauth2client.transport`,
+clean_headers = transport.clean_headers
+MemoryCache = transport.MemoryCache
+REFRESH_STATUS_CODES = transport.REFRESH_STATUS_CODES
+
+
+class SETTINGS(object):
+    """Settings namespace for globally defined values."""
+    env_name = None
+
+
+class Error(Exception):
+    """Base error for this module."""
+
+
+class FlowExchangeError(Error):
+    """Error trying to exchange an authorization grant for an access token."""
+
+
+class AccessTokenRefreshError(Error):
+    """Error trying to refresh an expired access token."""
+
+
+class HttpAccessTokenRefreshError(AccessTokenRefreshError):
+    """Error (with HTTP status) trying to refresh an expired access token."""
+    def __init__(self, *args, **kwargs):
+        super(HttpAccessTokenRefreshError, self).__init__(*args)
+        self.status = kwargs.get('status')
+
+
+class TokenRevokeError(Error):
+    """Error trying to revoke a token."""
+
+
+class UnknownClientSecretsFlowError(Error):
+    """The client secrets file called for an unknown type of OAuth 2.0 flow."""
+
+
+class AccessTokenCredentialsError(Error):
+    """Having only the access_token means no refresh is possible."""
+
+
+class VerifyJwtTokenError(Error):
+    """Could not retrieve certificates for validation."""
+
+
+class NonAsciiHeaderError(Error):
+    """Header names and values must be ASCII strings."""
+
+
+class ApplicationDefaultCredentialsError(Error):
+    """Error retrieving the Application Default Credentials."""
+
+
+class OAuth2DeviceCodeError(Error):
+    """Error trying to retrieve a device code."""
+
+
+class CryptoUnavailableError(Error, NotImplementedError):
+    """Raised when a crypto library is required, but none is available."""
+
+
+def _parse_expiry(expiry):
+    if expiry and isinstance(expiry, datetime.datetime):
+        return expiry.strftime(EXPIRY_FORMAT)
+    else:
+        return None
+
+
+class Credentials(object):
+    """Base class for all Credentials objects.
+
+    Subclasses must define an authorize() method that applies the credentials
+    to an HTTP transport.
+
+    Subclasses must also specify a classmethod named 'from_json' that takes a
+    JSON string as input and returns an instantiated Credentials object.
+    """
+
+    NON_SERIALIZED_MEMBERS = frozenset(['store'])
+
+    def authorize(self, http):
+        """Take an httplib2.Http instance (or equivalent) and authorizes it.
+
+        Authorizes it for the set of credentials, usually by replacing
+        http.request() with a method that adds in the appropriate headers and
+        then delegates to the original Http.request() method.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the refresh
+                  request.
+        """
+        raise NotImplementedError
+
+    def refresh(self, http):
+        """Forces a refresh of the access_token.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the refresh
+                  request.
+        """
+        raise NotImplementedError
+
+    def revoke(self, http):
+        """Revokes a refresh_token and makes the credentials void.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the revoke
+                  request.
+        """
+        raise NotImplementedError
+
+    def apply(self, headers):
+        """Add the authorization to the headers.
+
+        Args:
+            headers: dict, the headers to add the Authorization header to.
+        """
+        raise NotImplementedError
+
+    def _to_json(self, strip, to_serialize=None):
+        """Utility function that creates JSON repr. of a Credentials object.
+
+        Args:
+            strip: array, An array of names of members to exclude from the
+                   JSON.
+            to_serialize: dict, (Optional) The properties for this object
+                          that will be serialized. This allows callers to
+                          modify before serializing.
+
+        Returns:
+            string, a JSON representation of this instance, suitable to pass to
+            from_json().
+        """
+        curr_type = self.__class__
+        if to_serialize is None:
+            to_serialize = copy.copy(self.__dict__)
+        else:
+            # Assumes it is a str->str dictionary, so we don't deep copy.
+            to_serialize = copy.copy(to_serialize)
+        for member in strip:
+            if member in to_serialize:
+                del to_serialize[member]
+        to_serialize['token_expiry'] = _parse_expiry(
+            to_serialize.get('token_expiry'))
+        # Add in information we will need later to reconstitute this instance.
+        to_serialize['_class'] = curr_type.__name__
+        to_serialize['_module'] = curr_type.__module__
+        for key, val in to_serialize.items():
+            if isinstance(val, bytes):
+                to_serialize[key] = val.decode('utf-8')
+            if isinstance(val, set):
+                to_serialize[key] = list(val)
+        return json.dumps(to_serialize)
+
+    def to_json(self):
+        """Creating a JSON representation of an instance of Credentials.
+
+        Returns:
+            string, a JSON representation of this instance, suitable to pass to
+            from_json().
+        """
+        return self._to_json(self.NON_SERIALIZED_MEMBERS)
+
+    @classmethod
+    def new_from_json(cls, json_data):
+        """Utility class method to instantiate a Credentials subclass from JSON.
+
+        Expects the JSON string to have been produced by to_json().
+
+        Args:
+            json_data: string or bytes, JSON from to_json().
+
+        Returns:
+            An instance of the subclass of Credentials that was serialized with
+            to_json().
+        """
+        json_data_as_unicode = _helpers._from_bytes(json_data)
+        data = json.loads(json_data_as_unicode)
+        # Find and call the right classmethod from_json() to restore
+        # the object.
+        module_name = data['_module']
+        try:
+            module_obj = __import__(module_name)
+        except ImportError:
+            # In case there's an object from the old package structure,
+            # update it
+            module_name = module_name.replace('.googleapiclient', '')
+            module_obj = __import__(module_name)
+
+        module_obj = __import__(module_name,
+                                fromlist=module_name.split('.')[:-1])
+        kls = getattr(module_obj, data['_class'])
+        return kls.from_json(json_data_as_unicode)
+
+    @classmethod
+    def from_json(cls, unused_data):
+        """Instantiate a Credentials object from a JSON description of it.
+
+        The JSON should have been produced by calling .to_json() on the object.
+
+        Args:
+            unused_data: dict, A deserialized JSON object.
+
+        Returns:
+            An instance of a Credentials subclass.
+        """
+        return Credentials()
+
+
+class Flow(object):
+    """Base class for all Flow objects."""
+    pass
+
+
+class Storage(object):
+    """Base class for all Storage objects.
+
+    Store and retrieve a single credential. This class supports locking
+    such that multiple processes and threads can operate on a single
+    store.
+    """
+    def __init__(self, lock=None):
+        """Create a Storage instance.
+
+        Args:
+            lock: An optional threading.Lock-like object. Must implement at
+                  least acquire() and release(). Does not need to be
+                  re-entrant.
+        """
+        self._lock = lock
+
+    def acquire_lock(self):
+        """Acquires any lock necessary to access this Storage.
+
+        This lock is not reentrant.
+        """
+        if self._lock is not None:
+            self._lock.acquire()
+
+    def release_lock(self):
+        """Release the Storage lock.
+
+        Trying to release a lock that isn't held will result in a
+        RuntimeError in the case of a threading.Lock or multiprocessing.Lock.
+        """
+        if self._lock is not None:
+            self._lock.release()
+
+    def locked_get(self):
+        """Retrieve credential.
+
+        The Storage lock must be held when this is called.
+
+        Returns:
+            oauth2client.client.Credentials
+        """
+        raise NotImplementedError
+
+    def locked_put(self, credentials):
+        """Write a credential.
+
+        The Storage lock must be held when this is called.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        raise NotImplementedError
+
+    def locked_delete(self):
+        """Delete a credential.
+
+        The Storage lock must be held when this is called.
+        """
+        raise NotImplementedError
+
+    def get(self):
+        """Retrieve credential.
+
+        The Storage lock must *not* be held when this is called.
+
+        Returns:
+            oauth2client.client.Credentials
+        """
+        self.acquire_lock()
+        try:
+            return self.locked_get()
+        finally:
+            self.release_lock()
+
+    def put(self, credentials):
+        """Write a credential.
+
+        The Storage lock must be held when this is called.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        self.acquire_lock()
+        try:
+            self.locked_put(credentials)
+        finally:
+            self.release_lock()
+
+    def delete(self):
+        """Delete credential.
+
+        Frees any resources associated with storing the credential.
+        The Storage lock must *not* be held when this is called.
+
+        Returns:
+            None
+        """
+        self.acquire_lock()
+        try:
+            return self.locked_delete()
+        finally:
+            self.release_lock()
+
+
+def _update_query_params(uri, params):
+    """Updates a URI with new query parameters.
+
+    Args:
+        uri: string, A valid URI, with potential existing query parameters.
+        params: dict, A dictionary of query parameters.
+
+    Returns:
+        The same URI but with the new query parameters added.
+    """
+    parts = urllib.parse.urlparse(uri)
+    query_params = dict(urllib.parse.parse_qsl(parts.query))
+    query_params.update(params)
+    new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
+    return urllib.parse.urlunparse(new_parts)
+
+
+class OAuth2Credentials(Credentials):
+    """Credentials object for OAuth 2.0.
+
+    Credentials can be applied to an httplib2.Http object using the authorize()
+    method, which then adds the OAuth 2.0 access token to each request.
+
+    OAuth2Credentials objects may be safely pickled and unpickled.
+    """
+
+    @util.positional(8)
+    def __init__(self, access_token, client_id, client_secret, refresh_token,
+                 token_expiry, token_uri, user_agent, revoke_uri=None,
+                 id_token=None, token_response=None, scopes=None,
+                 token_info_uri=None):
+        """Create an instance of OAuth2Credentials.
+
+        This constructor is not usually called by the user, instead
+        OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
+
+        Args:
+            access_token: string, access token.
+            client_id: string, client identifier.
+            client_secret: string, client secret.
+            refresh_token: string, refresh token.
+            token_expiry: datetime, when the access_token expires.
+            token_uri: string, URI of token endpoint.
+            user_agent: string, The HTTP User-Agent to provide for this
+                        application.
+            revoke_uri: string, URI for revoke endpoint. Defaults to None; a
+                        token can't be revoked if this is None.
+            id_token: object, The identity of the resource owner.
+            token_response: dict, the decoded response to the token request.
+                            None if a token hasn't been requested yet. Stored
+                            because some providers (e.g. wordpress.com) include
+                            extra fields that clients may want.
+            scopes: list, authorized scopes for these credentials.
+          token_info_uri: string, the URI for the token info endpoint. Defaults
+                          to None; scopes can not be refreshed if this is None.
+
+        Notes:
+            store: callable, A callable that when passed a Credential
+                   will store the credential back to where it came from.
+                   This is needed to store the latest access_token if it
+                   has expired and been refreshed.
+        """
+        self.access_token = access_token
+        self.client_id = client_id
+        self.client_secret = client_secret
+        self.refresh_token = refresh_token
+        self.store = None
+        self.token_expiry = token_expiry
+        self.token_uri = token_uri
+        self.user_agent = user_agent
+        self.revoke_uri = revoke_uri
+        self.id_token = id_token
+        self.token_response = token_response
+        self.scopes = set(util.string_to_scopes(scopes or []))
+        self.token_info_uri = token_info_uri
+
+        # True if the credentials have been revoked or expired and can't be
+        # refreshed.
+        self.invalid = False
+
+    def authorize(self, http):
+        """Authorize an httplib2.Http instance with these credentials.
+
+        The modified http.request method will add authentication headers to
+        each request and will refresh access_tokens when a 401 is received on a
+        request. In addition the http.request method has a credentials
+        property, http.request.credentials, which is the Credentials object
+        that authorized it.
+
+        Args:
+            http: An instance of ``httplib2.Http`` or something that acts
+                  like it.
+
+        Returns:
+            A modified instance of http that was passed in.
+
+        Example::
+
+            h = httplib2.Http()
+            h = credentials.authorize(h)
+
+        You can't create a new OAuth subclass of httplib2.Authentication
+        because it never gets passed the absolute URI, which is needed for
+        signing. So instead we have to overload 'request' with a closure
+        that adds in the Authorization header and then calls the original
+        version of 'request()'.
+        """
+        transport.wrap_http_for_auth(self, http)
+        return http
+
+    def refresh(self, http):
+        """Forces a refresh of the access_token.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the refresh
+                  request.
+        """
+        self._refresh(http.request)
+
+    def revoke(self, http):
+        """Revokes a refresh_token and makes the credentials void.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the revoke
+                  request.
+        """
+        self._revoke(http.request)
+
+    def apply(self, headers):
+        """Add the authorization to the headers.
+
+        Args:
+            headers: dict, the headers to add the Authorization header to.
+        """
+        headers['Authorization'] = 'Bearer ' + self.access_token
+
+    def has_scopes(self, scopes):
+        """Verify that the credentials are authorized for the given scopes.
+
+        Returns True if the credentials authorized scopes contain all of the
+        scopes given.
+
+        Args:
+            scopes: list or string, the scopes to check.
+
+        Notes:
+            There are cases where the credentials are unaware of which scopes
+            are authorized. Notably, credentials obtained and stored before
+            this code was added will not have scopes, AccessTokenCredentials do
+            not have scopes. In both cases, you can use refresh_scopes() to
+            obtain the canonical set of scopes.
+        """
+        scopes = util.string_to_scopes(scopes)
+        return set(scopes).issubset(self.scopes)
+
+    def retrieve_scopes(self, http):
+        """Retrieves the canonical list of scopes for this access token.
+
+        Gets the scopes from the OAuth2 provider.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the refresh
+                  request.
+
+        Returns:
+            A set of strings containing the canonical list of scopes.
+        """
+        self._retrieve_scopes(http.request)
+        return self.scopes
+
+    @classmethod
+    def from_json(cls, json_data):
+        """Instantiate a Credentials object from a JSON description of it.
+
+        The JSON should have been produced by calling .to_json() on the object.
+
+        Args:
+            json_data: string or bytes, JSON to deserialize.
+
+        Returns:
+            An instance of a Credentials subclass.
+        """
+        data = json.loads(_helpers._from_bytes(json_data))
+        if (data.get('token_expiry') and
+                not isinstance(data['token_expiry'], datetime.datetime)):
+            try:
+                data['token_expiry'] = datetime.datetime.strptime(
+                    data['token_expiry'], EXPIRY_FORMAT)
+            except ValueError:
+                data['token_expiry'] = None
+        retval = cls(
+            data['access_token'],
+            data['client_id'],
+            data['client_secret'],
+            data['refresh_token'],
+            data['token_expiry'],
+            data['token_uri'],
+            data['user_agent'],
+            revoke_uri=data.get('revoke_uri', None),
+            id_token=data.get('id_token', None),
+            token_response=data.get('token_response', None),
+            scopes=data.get('scopes', None),
+            token_info_uri=data.get('token_info_uri', None))
+        retval.invalid = data['invalid']
+        return retval
+
+    @property
+    def access_token_expired(self):
+        """True if the credential is expired or invalid.
+
+        If the token_expiry isn't set, we assume the token doesn't expire.
+        """
+        if self.invalid:
+            return True
+
+        if not self.token_expiry:
+            return False
+
+        now = _UTCNOW()
+        if now >= self.token_expiry:
+            logger.info('access_token is expired. Now: %s, token_expiry: %s',
+                        now, self.token_expiry)
+            return True
+        return False
+
+    def get_access_token(self, http=None):
+        """Return the access token and its expiration information.
+
+        If the token does not exist, get one.
+        If the token expired, refresh it.
+        """
+        if not self.access_token or self.access_token_expired:
+            if not http:
+                http = transport.get_http_object()
+            self.refresh(http)
+        return AccessTokenInfo(access_token=self.access_token,
+                               expires_in=self._expires_in())
+
+    def set_store(self, store):
+        """Set the Storage for the credential.
+
+        Args:
+            store: Storage, an implementation of Storage object.
+                   This is needed to store the latest access_token if it
+                   has expired and been refreshed. This implementation uses
+                   locking to check for updates before updating the
+                   access_token.
+        """
+        self.store = store
+
+    def _expires_in(self):
+        """Return the number of seconds until this token expires.
+
+        If token_expiry is in the past, this method will return 0, meaning the
+        token has already expired.
+
+        If token_expiry is None, this method will return None. Note that
+        returning 0 in such a case would not be fair: the token may still be
+        valid; we just don't know anything about it.
+        """
+        if self.token_expiry:
+            now = _UTCNOW()
+            if self.token_expiry > now:
+                time_delta = self.token_expiry - now
+                # TODO(orestica): return time_delta.total_seconds()
+                # once dropping support for Python 2.6
+                return time_delta.days * 86400 + time_delta.seconds
+            else:
+                return 0
+
+    def _updateFromCredential(self, other):
+        """Update this Credential from another instance."""
+        self.__dict__.update(other.__getstate__())
+
+    def __getstate__(self):
+        """Trim the state down to something that can be pickled."""
+        d = copy.copy(self.__dict__)
+        del d['store']
+        return d
+
+    def __setstate__(self, state):
+        """Reconstitute the state of the object from being pickled."""
+        self.__dict__.update(state)
+        self.store = None
+
+    def _generate_refresh_request_body(self):
+        """Generate the body that will be used in the refresh request."""
+        body = urllib.parse.urlencode({
+            'grant_type': 'refresh_token',
+            'client_id': self.client_id,
+            'client_secret': self.client_secret,
+            'refresh_token': self.refresh_token,
+        })
+        return body
+
+    def _generate_refresh_request_headers(self):
+        """Generate the headers that will be used in the refresh request."""
+        headers = {
+            'content-type': 'application/x-www-form-urlencoded',
+        }
+
+        if self.user_agent is not None:
+            headers['user-agent'] = self.user_agent
+
+        return headers
+
+    def _refresh(self, http_request):
+        """Refreshes the access_token.
+
+        This method first checks by reading the Storage object if available.
+        If a refresh is still needed, it holds the Storage lock until the
+        refresh is completed.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          refresh request.
+
+        Raises:
+            HttpAccessTokenRefreshError: When the refresh fails.
+        """
+        if not self.store:
+            self._do_refresh_request(http_request)
+        else:
+            self.store.acquire_lock()
+            try:
+                new_cred = self.store.locked_get()
+
+                if (new_cred and not new_cred.invalid and
+                        new_cred.access_token != self.access_token and
+                        not new_cred.access_token_expired):
+                    logger.info('Updated access_token read from Storage')
+                    self._updateFromCredential(new_cred)
+                else:
+                    self._do_refresh_request(http_request)
+            finally:
+                self.store.release_lock()
+
+    def _do_refresh_request(self, http_request):
+        """Refresh the access_token using the refresh_token.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          refresh request.
+
+        Raises:
+            HttpAccessTokenRefreshError: When the refresh fails.
+        """
+        body = self._generate_refresh_request_body()
+        headers = self._generate_refresh_request_headers()
+
+        logger.info('Refreshing access_token')
+        resp, content = http_request(
+            self.token_uri, method='POST', body=body, headers=headers)
+        content = _helpers._from_bytes(content)
+        if resp.status == http_client.OK:
+            d = json.loads(content)
+            self.token_response = d
+            self.access_token = d['access_token']
+            self.refresh_token = d.get('refresh_token', self.refresh_token)
+            if 'expires_in' in d:
+                delta = datetime.timedelta(seconds=int(d['expires_in']))
+                self.token_expiry = delta + _UTCNOW()
+            else:
+                self.token_expiry = None
+            if 'id_token' in d:
+                self.id_token = _extract_id_token(d['id_token'])
+            else:
+                self.id_token = None
+            # On temporary refresh errors, the user does not actually have to
+            # re-authorize, so we unflag here.
+            self.invalid = False
+            if self.store:
+                self.store.locked_put(self)
+        else:
+            # An {'error':...} response body means the token is expired or
+            # revoked, so we flag the credentials as such.
+            logger.info('Failed to retrieve access token: %s', content)
+            error_msg = 'Invalid response {0}.'.format(resp['status'])
+            try:
+                d = json.loads(content)
+                if 'error' in d:
+                    error_msg = d['error']
+                    if 'error_description' in d:
+                        error_msg += ': ' + d['error_description']
+                    self.invalid = True
+                    if self.store is not None:
+                        self.store.locked_put(self)
+            except (TypeError, ValueError):
+                pass
+            raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
+
+    def _revoke(self, http_request):
+        """Revokes this credential and deletes the stored copy (if it exists).
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          revoke request.
+        """
+        self._do_revoke(http_request, self.refresh_token or self.access_token)
+
+    def _do_revoke(self, http_request, token):
+        """Revokes this credential and deletes the stored copy (if it exists).
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          refresh request.
+            token: A string used as the token to be revoked. Can be either an
+                   access_token or refresh_token.
+
+        Raises:
+            TokenRevokeError: If the revoke request does not return with a
+                              200 OK.
+        """
+        logger.info('Revoking token')
+        query_params = {'token': token}
+        token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
+        resp, content = http_request(token_revoke_uri)
+        if resp.status == http_client.OK:
+            self.invalid = True
+        else:
+            error_msg = 'Invalid response {0}.'.format(resp.status)
+            try:
+                d = json.loads(_helpers._from_bytes(content))
+                if 'error' in d:
+                    error_msg = d['error']
+            except (TypeError, ValueError):
+                pass
+            raise TokenRevokeError(error_msg)
+
+        if self.store:
+            self.store.delete()
+
+    def _retrieve_scopes(self, http_request):
+        """Retrieves the list of authorized scopes from the OAuth2 provider.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          revoke request.
+        """
+        self._do_retrieve_scopes(http_request, self.access_token)
+
+    def _do_retrieve_scopes(self, http_request, token):
+        """Retrieves the list of authorized scopes from the OAuth2 provider.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          refresh request.
+            token: A string used as the token to identify the credentials to
+                   the provider.
+
+        Raises:
+            Error: When refresh fails, indicating the the access token is
+                   invalid.
+        """
+        logger.info('Refreshing scopes')
+        query_params = {'access_token': token, 'fields': 'scope'}
+        token_info_uri = _update_query_params(self.token_info_uri,
+                                              query_params)
+        resp, content = http_request(token_info_uri)
+        content = _helpers._from_bytes(content)
+        if resp.status == http_client.OK:
+            d = json.loads(content)
+            self.scopes = set(util.string_to_scopes(d.get('scope', '')))
+        else:
+            error_msg = 'Invalid response {0}.'.format(resp.status)
+            try:
+                d = json.loads(content)
+                if 'error_description' in d:
+                    error_msg = d['error_description']
+            except (TypeError, ValueError):
+                pass
+            raise Error(error_msg)
+
+
+class AccessTokenCredentials(OAuth2Credentials):
+    """Credentials object for OAuth 2.0.
+
+    Credentials can be applied to an httplib2.Http object using the
+    authorize() method, which then signs each request from that object
+    with the OAuth 2.0 access token. This set of credentials is for the
+    use case where you have acquired an OAuth 2.0 access_token from
+    another place such as a JavaScript client or another web
+    application, and wish to use it from Python. Because only the
+    access_token is present it can not be refreshed and will in time
+    expire.
+
+    AccessTokenCredentials objects may be safely pickled and unpickled.
+
+    Usage::
+
+        credentials = AccessTokenCredentials('<an access token>',
+            'my-user-agent/1.0')
+        http = httplib2.Http()
+        http = credentials.authorize(http)
+
+    Raises:
+        AccessTokenCredentialsExpired: raised when the access_token expires or
+                                       is revoked.
+    """
+
+    def __init__(self, access_token, user_agent, revoke_uri=None):
+        """Create an instance of OAuth2Credentials
+
+        This is one of the few types if Credentials that you should contrust,
+        Credentials objects are usually instantiated by a Flow.
+
+        Args:
+            access_token: string, access token.
+            user_agent: string, The HTTP User-Agent to provide for this
+                        application.
+            revoke_uri: string, URI for revoke endpoint. Defaults to None; a
+                        token can't be revoked if this is None.
+        """
+        super(AccessTokenCredentials, self).__init__(
+            access_token,
+            None,
+            None,
+            None,
+            None,
+            None,
+            user_agent,
+            revoke_uri=revoke_uri)
+
+    @classmethod
+    def from_json(cls, json_data):
+        data = json.loads(_helpers._from_bytes(json_data))
+        retval = AccessTokenCredentials(
+            data['access_token'],
+            data['user_agent'])
+        return retval
+
+    def _refresh(self, http_request):
+        raise AccessTokenCredentialsError(
+            'The access_token is expired or invalid and can\'t be refreshed.')
+
+    def _revoke(self, http_request):
+        """Revokes the access_token and deletes the store if available.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          revoke request.
+        """
+        self._do_revoke(http_request, self.access_token)
+
+
+def _detect_gce_environment():
+    """Determine if the current environment is Compute Engine.
+
+    Returns:
+        Boolean indicating whether or not the current environment is Google
+        Compute Engine.
+    """
+    # NOTE: The explicit ``timeout`` is a workaround. The underlying
+    #       issue is that resolving an unknown host on some networks will take
+    #       20-30 seconds; making this timeout short fixes the issue, but
+    #       could lead to false negatives in the event that we are on GCE, but
+    #       the metadata resolution was particularly slow. The latter case is
+    #       "unlikely".
+    connection = six.moves.http_client.HTTPConnection(
+        _GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT)
+
+    try:
+        headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
+        connection.request('GET', '/', headers=headers)
+        response = connection.getresponse()
+        if response.status == http_client.OK:
+            return (response.getheader(_METADATA_FLAVOR_HEADER) ==
+                    _DESIRED_METADATA_FLAVOR)
+    except socket.error:  # socket.timeout or socket.error(64, 'Host is down')
+        logger.info('Timeout attempting to reach GCE metadata service.')
+        return False
+    finally:
+        connection.close()
+
+
+def _in_gae_environment():
+    """Detects if the code is running in the App Engine environment.
+
+    Returns:
+        True if running in the GAE environment, False otherwise.
+    """
+    if SETTINGS.env_name is not None:
+        return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL')
+
+    try:
+        import google.appengine  # noqa: unused import
+    except ImportError:
+        pass
+    else:
+        server_software = os.environ.get(_SERVER_SOFTWARE, '')
+        if server_software.startswith('Google App Engine/'):
+            SETTINGS.env_name = 'GAE_PRODUCTION'
+            return True
+        elif server_software.startswith('Development/'):
+            SETTINGS.env_name = 'GAE_LOCAL'
+            return True
+
+    return False
+
+
+def _in_gce_environment():
+    """Detect if the code is running in the Compute Engine environment.
+
+    Returns:
+        True if running in the GCE environment, False otherwise.
+    """
+    if SETTINGS.env_name is not None:
+        return SETTINGS.env_name == 'GCE_PRODUCTION'
+
+    if NO_GCE_CHECK != 'True' and _detect_gce_environment():
+        SETTINGS.env_name = 'GCE_PRODUCTION'
+        return True
+    return False
+
+
+class GoogleCredentials(OAuth2Credentials):
+    """Application Default Credentials for use in calling Google APIs.
+
+    The Application Default Credentials are being constructed as a function of
+    the environment where the code is being run.
+    More details can be found on this page:
+    https://developers.google.com/accounts/docs/application-default-credentials
+
+    Here is an example of how to use the Application Default Credentials for a
+    service that requires authentication::
+
+        from googleapiclient.discovery import build
+        from oauth2client.client import GoogleCredentials
+
+        credentials = GoogleCredentials.get_application_default()
+        service = build('compute', 'v1', credentials=credentials)
+
+        PROJECT = 'bamboo-machine-422'
+        ZONE = 'us-central1-a'
+        request = service.instances().list(project=PROJECT, zone=ZONE)
+        response = request.execute()
+
+        print(response)
+    """
+
+    NON_SERIALIZED_MEMBERS = (
+        frozenset(['_private_key']) |
+        OAuth2Credentials.NON_SERIALIZED_MEMBERS)
+    """Members that aren't serialized when object is converted to JSON."""
+
+    def __init__(self, access_token, client_id, client_secret, refresh_token,
+                 token_expiry, token_uri, user_agent,
+                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+        """Create an instance of GoogleCredentials.
+
+        This constructor is not usually called by the user, instead
+        GoogleCredentials objects are instantiated by
+        GoogleCredentials.from_stream() or
+        GoogleCredentials.get_application_default().
+
+        Args:
+            access_token: string, access token.
+            client_id: string, client identifier.
+            client_secret: string, client secret.
+            refresh_token: string, refresh token.
+            token_expiry: datetime, when the access_token expires.
+            token_uri: string, URI of token endpoint.
+            user_agent: string, The HTTP User-Agent to provide for this
+                        application.
+            revoke_uri: string, URI for revoke endpoint. Defaults to
+                        oauth2client.GOOGLE_REVOKE_URI; a token can't be
+                        revoked if this is None.
+        """
+        super(GoogleCredentials, self).__init__(
+            access_token, client_id, client_secret, refresh_token,
+            token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
+
+    def create_scoped_required(self):
+        """Whether this Credentials object is scopeless.
+
+        create_scoped(scopes) method needs to be called in order to create
+        a Credentials object for API calls.
+        """
+        return False
+
+    def create_scoped(self, scopes):
+        """Create a Credentials object for the given scopes.
+
+        The Credentials type is preserved.
+        """
+        return self
+
+    @classmethod
+    def from_json(cls, json_data):
+        # TODO(issue 388): eliminate the circularity that is the reason for
+        #                  this non-top-level import.
+        from oauth2client import service_account
+        data = json.loads(_helpers._from_bytes(json_data))
+
+        # We handle service_account.ServiceAccountCredentials since it is a
+        # possible return type of GoogleCredentials.get_application_default()
+        if (data['_module'] == 'oauth2client.service_account' and
+                data['_class'] == 'ServiceAccountCredentials'):
+            return service_account.ServiceAccountCredentials.from_json(data)
+        elif (data['_module'] == 'oauth2client.service_account' and
+                data['_class'] == '_JWTAccessCredentials'):
+            return service_account._JWTAccessCredentials.from_json(data)
+
+        token_expiry = _parse_expiry(data.get('token_expiry'))
+        google_credentials = cls(
+            data['access_token'],
+            data['client_id'],
+            data['client_secret'],
+            data['refresh_token'],
+            token_expiry,
+            data['token_uri'],
+            data['user_agent'],
+            revoke_uri=data.get('revoke_uri', None))
+        google_credentials.invalid = data['invalid']
+        return google_credentials
+
+    @property
+    def serialization_data(self):
+        """Get the fields and values identifying the current credentials."""
+        return {
+            'type': 'authorized_user',
+            'client_id': self.client_id,
+            'client_secret': self.client_secret,
+            'refresh_token': self.refresh_token
+        }
+
+    @staticmethod
+    def _implicit_credentials_from_gae():
+        """Attempts to get implicit credentials in Google App Engine env.
+
+        If the current environment is not detected as App Engine, returns None,
+        indicating no Google App Engine credentials can be detected from the
+        current environment.
+
+        Returns:
+            None, if not in GAE, else an appengine.AppAssertionCredentials
+            object.
+        """
+        if not _in_gae_environment():
+            return None
+
+        return _get_application_default_credential_GAE()
+
+    @staticmethod
+    def _implicit_credentials_from_gce():
+        """Attempts to get implicit credentials in Google Compute Engine env.
+
+        If the current environment is not detected as Compute Engine, returns
+        None, indicating no Google Compute Engine credentials can be detected
+        from the current environment.
+
+        Returns:
+            None, if not in GCE, else a gce.AppAssertionCredentials object.
+        """
+        if not _in_gce_environment():
+            return None
+
+        return _get_application_default_credential_GCE()
+
+    @staticmethod
+    def _implicit_credentials_from_files():
+        """Attempts to get implicit credentials from local credential files.
+
+        First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS
+        is set with a filename and then falls back to a configuration file (the
+        "well known" file) associated with the 'gcloud' command line tool.
+
+        Returns:
+            Credentials object associated with the
+            GOOGLE_APPLICATION_CREDENTIALS file or the "well known" file if
+            either exist. If neither file is define, returns None, indicating
+            no credentials from a file can detected from the current
+            environment.
+        """
+        credentials_filename = _get_environment_variable_file()
+        if not credentials_filename:
+            credentials_filename = _get_well_known_file()
+            if os.path.isfile(credentials_filename):
+                extra_help = (' (produced automatically when running'
+                              ' "gcloud auth login" command)')
+            else:
+                credentials_filename = None
+        else:
+            extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS +
+                          ' environment variable)')
+
+        if not credentials_filename:
+            return
+
+        # If we can read the credentials from a file, we don't need to know
+        # what environment we are in.
+        SETTINGS.env_name = DEFAULT_ENV_NAME
+
+        try:
+            return _get_application_default_credential_from_file(
+                credentials_filename)
+        except (ApplicationDefaultCredentialsError, ValueError) as error:
+            _raise_exception_for_reading_json(credentials_filename,
+                                              extra_help, error)
+
+    @classmethod
+    def _get_implicit_credentials(cls):
+        """Gets credentials implicitly from the environment.
+
+        Checks environment in order of precedence:
+        - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to
+          a file with stored credentials information.
+        - Stored "well known" file associated with `gcloud` command line tool.
+        - Google App Engine (production and testing)
+        - Google Compute Engine production environment.
+
+        Raises:
+            ApplicationDefaultCredentialsError: raised when the credentials
+                                                fail to be retrieved.
+        """
+        # Environ checks (in order).
+        environ_checkers = [
+            cls._implicit_credentials_from_files,
+            cls._implicit_credentials_from_gae,
+            cls._implicit_credentials_from_gce,
+        ]
+
+        for checker in environ_checkers:
+            credentials = checker()
+            if credentials is not None:
+                return credentials
+
+        # If no credentials, fail.
+        raise ApplicationDefaultCredentialsError(ADC_HELP_MSG)
+
+    @staticmethod
+    def get_application_default():
+        """Get the Application Default Credentials for the current environment.
+
+        Raises:
+            ApplicationDefaultCredentialsError: raised when the credentials
+                                                fail to be retrieved.
+        """
+        return GoogleCredentials._get_implicit_credentials()
+
+    @staticmethod
+    def from_stream(credential_filename):
+        """Create a Credentials object by reading information from a file.
+
+        It returns an object of type GoogleCredentials.
+
+        Args:
+            credential_filename: the path to the file from where the
+                                 credentials are to be read
+
+        Raises:
+            ApplicationDefaultCredentialsError: raised when the credentials
+                                                fail to be retrieved.
+        """
+        if credential_filename and os.path.isfile(credential_filename):
+            try:
+                return _get_application_default_credential_from_file(
+                    credential_filename)
+            except (ApplicationDefaultCredentialsError, ValueError) as error:
+                extra_help = (' (provided as parameter to the '
+                              'from_stream() method)')
+                _raise_exception_for_reading_json(credential_filename,
+                                                  extra_help,
+                                                  error)
+        else:
+            raise ApplicationDefaultCredentialsError(
+                'The parameter passed to the from_stream() '
+                'method should point to a file.')
+
+
+def _save_private_file(filename, json_contents):
+    """Saves a file with read-write permissions on for the owner.
+
+    Args:
+        filename: String. Absolute path to file.
+        json_contents: JSON serializable object to be saved.
+    """
+    temp_filename = tempfile.mktemp()
+    file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600)
+    with os.fdopen(file_desc, 'w') as file_handle:
+        json.dump(json_contents, file_handle, sort_keys=True,
+                  indent=2, separators=(',', ': '))
+    shutil.move(temp_filename, filename)
+
+
+def save_to_well_known_file(credentials, well_known_file=None):
+    """Save the provided GoogleCredentials to the well known file.
+
+    Args:
+        credentials: the credentials to be saved to the well known file;
+                     it should be an instance of GoogleCredentials
+        well_known_file: the name of the file where the credentials are to be
+                         saved; this parameter is supposed to be used for
+                         testing only
+    """
+    # TODO(orestica): move this method to tools.py
+    # once the argparse import gets fixed (it is not present in Python 2.6)
+
+    if well_known_file is None:
+        well_known_file = _get_well_known_file()
+
+    config_dir = os.path.dirname(well_known_file)
+    if not os.path.isdir(config_dir):
+        raise OSError(
+            'Config directory does not exist: {0}'.format(config_dir))
+
+    credentials_data = credentials.serialization_data
+    _save_private_file(well_known_file, credentials_data)
+
+
+def _get_environment_variable_file():
+    application_default_credential_filename = (
+        os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, None))
+
+    if application_default_credential_filename:
+        if os.path.isfile(application_default_credential_filename):
+            return application_default_credential_filename
+        else:
+            raise ApplicationDefaultCredentialsError(
+                'File ' + application_default_credential_filename +
+                ' (pointed by ' +
+                GOOGLE_APPLICATION_CREDENTIALS +
+                ' environment variable) does not exist!')
+
+
+def _get_well_known_file():
+    """Get the well known file produced by command 'gcloud auth login'."""
+    # TODO(orestica): Revisit this method once gcloud provides a better way
+    # of pinpointing the exact location of the file.
+    default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR)
+    if default_config_dir is None:
+        if os.name == 'nt':
+            try:
+                default_config_dir = os.path.join(os.environ['APPDATA'],
+                                                  _CLOUDSDK_CONFIG_DIRECTORY)
+            except KeyError:
+                # This should never happen unless someone is really
+                # messing with things.
+                drive = os.environ.get('SystemDrive', 'C:')
+                default_config_dir = os.path.join(drive, '\\',
+                                                  _CLOUDSDK_CONFIG_DIRECTORY)
+        else:
+            default_config_dir = os.path.join(os.path.expanduser('~'),
+                                              '.config',
+                                              _CLOUDSDK_CONFIG_DIRECTORY)
+
+    return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE)
+
+
+def _get_application_default_credential_from_file(filename):
+    """Build the Application Default Credentials from file."""
+    # read the credentials from the file
+    with open(filename) as file_obj:
+        client_credentials = json.load(file_obj)
+
+    credentials_type = client_credentials.get('type')
+    if credentials_type == AUTHORIZED_USER:
+        required_fields = set(['client_id', 'client_secret', 'refresh_token'])
+    elif credentials_type == SERVICE_ACCOUNT:
+        required_fields = set(['client_id', 'client_email', 'private_key_id',
+                               'private_key'])
+    else:
+        raise ApplicationDefaultCredentialsError(
+            "'type' field should be defined (and have one of the '" +
+            AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
+
+    missing_fields = required_fields.difference(client_credentials.keys())
+
+    if missing_fields:
+        _raise_exception_for_missing_fields(missing_fields)
+
+    if client_credentials['type'] == AUTHORIZED_USER:
+        return GoogleCredentials(
+            access_token=None,
+            client_id=client_credentials['client_id'],
+            client_secret=client_credentials['client_secret'],
+            refresh_token=client_credentials['refresh_token'],
+            token_expiry=None,
+            token_uri=oauth2client.GOOGLE_TOKEN_URI,
+            user_agent='Python client library')
+    else:  # client_credentials['type'] == SERVICE_ACCOUNT
+        from oauth2client import service_account
+        return service_account._JWTAccessCredentials.from_json_keyfile_dict(
+            client_credentials)
+
+
+def _raise_exception_for_missing_fields(missing_fields):
+    raise ApplicationDefaultCredentialsError(
+        'The following field(s) must be defined: ' + ', '.join(missing_fields))
+
+
+def _raise_exception_for_reading_json(credential_file,
+                                      extra_help,
+                                      error):
+    raise ApplicationDefaultCredentialsError(
+        'An error was encountered while reading json file: ' +
+        credential_file + extra_help + ': ' + str(error))
+
+
+def _get_application_default_credential_GAE():
+    from oauth2client.contrib.appengine import AppAssertionCredentials
+
+    return AppAssertionCredentials([])
+
+
+def _get_application_default_credential_GCE():
+    from oauth2client.contrib.gce import AppAssertionCredentials
+
+    return AppAssertionCredentials()
+
+
+class AssertionCredentials(GoogleCredentials):
+    """Abstract Credentials object used for OAuth 2.0 assertion grants.
+
+    This credential does not require a flow to instantiate because it
+    represents a two legged flow, and therefore has all of the required
+    information to generate and refresh its own access tokens. It must
+    be subclassed to generate the appropriate assertion string.
+
+    AssertionCredentials objects may be safely pickled and unpickled.
+    """
+
+    @util.positional(2)
+    def __init__(self, assertion_type, user_agent=None,
+                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+                 **unused_kwargs):
+        """Constructor for AssertionFlowCredentials.
+
+        Args:
+            assertion_type: string, assertion type that will be declared to the
+                            auth server
+            user_agent: string, The HTTP User-Agent to provide for this
+                        application.
+            token_uri: string, URI for token endpoint. For convenience defaults
+                       to Google's endpoints but any OAuth 2.0 provider can be
+                       used.
+            revoke_uri: string, URI for revoke endpoint.
+        """
+        super(AssertionCredentials, self).__init__(
+            None,
+            None,
+            None,
+            None,
+            None,
+            token_uri,
+            user_agent,
+            revoke_uri=revoke_uri)
+        self.assertion_type = assertion_type
+
+    def _generate_refresh_request_body(self):
+        assertion = self._generate_assertion()
+
+        body = urllib.parse.urlencode({
+            'assertion': assertion,
+            'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+        })
+
+        return body
+
+    def _generate_assertion(self):
+        """Generate assertion string to be used in the access token request."""
+        raise NotImplementedError
+
+    def _revoke(self, http_request):
+        """Revokes the access_token and deletes the store if available.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          revoke request.
+        """
+        self._do_revoke(http_request, self.access_token)
+
+    def sign_blob(self, blob):
+        """Cryptographically sign a blob (of bytes).
+
+        Args:
+            blob: bytes, Message to be signed.
+
+        Returns:
+            tuple, A pair of the private key ID used to sign the blob and
+            the signed contents.
+        """
+        raise NotImplementedError('This method is abstract.')
+
+
+def _require_crypto_or_die():
+    """Ensure we have a crypto library, or throw CryptoUnavailableError.
+
+    The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
+    to be available in order to function, but these are optional
+    dependencies.
+    """
+    if not HAS_CRYPTO:
+        raise CryptoUnavailableError('No crypto library available')
+
+
+@util.positional(2)
+def verify_id_token(id_token, audience, http=None,
+                    cert_uri=ID_TOKEN_VERIFICATION_CERTS):
+    """Verifies a signed JWT id_token.
+
+    This function requires PyOpenSSL and because of that it does not work on
+    App Engine.
+
+    Args:
+        id_token: string, A Signed JWT.
+        audience: string, The audience 'aud' that the token should be for.
+        http: httplib2.Http, instance to use to make the HTTP request. Callers
+              should supply an instance that has caching enabled.
+        cert_uri: string, URI of the certificates in JSON format to
+                  verify the JWT against.
+
+    Returns:
+        The deserialized JSON in the JWT.
+
+    Raises:
+        oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
+        CryptoUnavailableError: if no crypto library is available.
+    """
+    _require_crypto_or_die()
+    if http is None:
+        http = transport.get_cached_http()
+
+    resp, content = http.request(cert_uri)
+    if resp.status == http_client.OK:
+        certs = json.loads(_helpers._from_bytes(content))
+        return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
+    else:
+        raise VerifyJwtTokenError('Status code: {0}'.format(resp.status))
+
+
+def _extract_id_token(id_token):
+    """Extract the JSON payload from a JWT.
+
+    Does the extraction w/o checking the signature.
+
+    Args:
+        id_token: string or bytestring, OAuth 2.0 id_token.
+
+    Returns:
+        object, The deserialized JSON payload.
+    """
+    if type(id_token) == bytes:
+        segments = id_token.split(b'.')
+    else:
+        segments = id_token.split(u'.')
+
+    if len(segments) != 3:
+        raise VerifyJwtTokenError(
+            'Wrong number of segments in token: {0}'.format(id_token))
+
+    return json.loads(
+        _helpers._from_bytes(_helpers._urlsafe_b64decode(segments[1])))
+
+
+def _parse_exchange_token_response(content):
+    """Parses response of an exchange token request.
+
+    Most providers return JSON but some (e.g. Facebook) return a
+    url-encoded string.
+
+    Args:
+        content: The body of a response
+
+    Returns:
+        Content as a dictionary object. Note that the dict could be empty,
+        i.e. {}. That basically indicates a failure.
+    """
+    resp = {}
+    content = _helpers._from_bytes(content)
+    try:
+        resp = json.loads(content)
+    except Exception:
+        # different JSON libs raise different exceptions,
+        # so we just do a catch-all here
+        resp = dict(urllib.parse.parse_qsl(content))
+
+    # some providers respond with 'expires', others with 'expires_in'
+    if resp and 'expires' in resp:
+        resp['expires_in'] = resp.pop('expires')
+
+    return resp
+
+
+@util.positional(4)
+def credentials_from_code(client_id, client_secret, scope, code,
+                          redirect_uri='postmessage', http=None,
+                          user_agent=None,
+                          token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                          auth_uri=oauth2client.GOOGLE_AUTH_URI,
+                          revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+                          device_uri=oauth2client.GOOGLE_DEVICE_URI,
+                          token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI):
+    """Exchanges an authorization code for an OAuth2Credentials object.
+
+    Args:
+        client_id: string, client identifier.
+        client_secret: string, client secret.
+        scope: string or iterable of strings, scope(s) to request.
+        code: string, An authorization code, most likely passed down from
+              the client
+        redirect_uri: string, this is generally set to 'postmessage' to match
+                      the redirect_uri that the client specified
+        http: httplib2.Http, optional http instance to use to do the fetch
+        token_uri: string, URI for token endpoint. For convenience defaults
+                   to Google's endpoints but any OAuth 2.0 provider can be
+                   used.
+        auth_uri: string, URI for authorization endpoint. For convenience
+                  defaults to Google's endpoints but any OAuth 2.0 provider
+                  can be used.
+        revoke_uri: string, URI for revoke endpoint. For convenience
+                    defaults to Google's endpoints but any OAuth 2.0 provider
+                    can be used.
+        device_uri: string, URI for device authorization endpoint. For
+                    convenience defaults to Google's endpoints but any OAuth
+                    2.0 provider can be used.
+
+    Returns:
+        An OAuth2Credentials object.
+
+    Raises:
+        FlowExchangeError if the authorization code cannot be exchanged for an
+        access token
+    """
+    flow = OAuth2WebServerFlow(client_id, client_secret, scope,
+                               redirect_uri=redirect_uri,
+                               user_agent=user_agent, auth_uri=auth_uri,
+                               token_uri=token_uri, revoke_uri=revoke_uri,
+                               device_uri=device_uri,
+                               token_info_uri=token_info_uri)
+
+    credentials = flow.step2_exchange(code, http=http)
+    return credentials
+
+
+@util.positional(3)
+def credentials_from_clientsecrets_and_code(filename, scope, code,
+                                            message=None,
+                                            redirect_uri='postmessage',
+                                            http=None,
+                                            cache=None,
+                                            device_uri=None):
+    """Returns OAuth2Credentials from a clientsecrets file and an auth code.
+
+    Will create the right kind of Flow based on the contents of the
+    clientsecrets file or will raise InvalidClientSecretsError for unknown
+    types of Flows.
+
+    Args:
+        filename: string, File name of clientsecrets.
+        scope: string or iterable of strings, scope(s) to request.
+        code: string, An authorization code, most likely passed down from
+              the client
+        message: string, A friendly string to display to the user if the
+                 clientsecrets file is missing or invalid. If message is
+                 provided then sys.exit will be called in the case of an error.
+                 If message in not provided then
+                 clientsecrets.InvalidClientSecretsError will be raised.
+        redirect_uri: string, this is generally set to 'postmessage' to match
+                      the redirect_uri that the client specified
+        http: httplib2.Http, optional http instance to use to do the fetch
+        cache: An optional cache service client that implements get() and set()
+               methods. See clientsecrets.loadfile() for details.
+        device_uri: string, OAuth 2.0 device authorization endpoint
+
+    Returns:
+        An OAuth2Credentials object.
+
+    Raises:
+        FlowExchangeError: if the authorization code cannot be exchanged for an
+                           access token
+        UnknownClientSecretsFlowError: if the file describes an unknown kind
+                                       of Flow.
+        clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
+                                                 invalid.
+    """
+    flow = flow_from_clientsecrets(filename, scope, message=message,
+                                   cache=cache, redirect_uri=redirect_uri,
+                                   device_uri=device_uri)
+    credentials = flow.step2_exchange(code, http=http)
+    return credentials
+
+
+class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
+        'device_code', 'user_code', 'interval', 'verification_url',
+        'user_code_expiry'))):
+    """Intermediate information the OAuth2 for devices flow."""
+
+    @classmethod
+    def FromResponse(cls, response):
+        """Create a DeviceFlowInfo from a server response.
+
+        The response should be a dict containing entries as described here:
+
+        http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1
+        """
+        # device_code, user_code, and verification_url are required.
+        kwargs = {
+            'device_code': response['device_code'],
+            'user_code': response['user_code'],
+        }
+        # The response may list the verification address as either
+        # verification_url or verification_uri, so we check for both.
+        verification_url = response.get(
+            'verification_url', response.get('verification_uri'))
+        if verification_url is None:
+            raise OAuth2DeviceCodeError(
+                'No verification_url provided in server response')
+        kwargs['verification_url'] = verification_url
+        # expires_in and interval are optional.
+        kwargs.update({
+            'interval': response.get('interval'),
+            'user_code_expiry': None,
+        })
+        if 'expires_in' in response:
+            kwargs['user_code_expiry'] = (
+                _UTCNOW() +
+                datetime.timedelta(seconds=int(response['expires_in'])))
+        return cls(**kwargs)
+
+
+def _oauth2_web_server_flow_params(kwargs):
+    """Configures redirect URI parameters for OAuth2WebServerFlow."""
+    params = {
+        'access_type': 'offline',
+        'response_type': 'code',
+    }
+
+    params.update(kwargs)
+
+    # Check for the presence of the deprecated approval_prompt param and
+    # warn appropriately.
+    approval_prompt = params.get('approval_prompt')
+    if approval_prompt is not None:
+        logger.warning(
+            'The approval_prompt parameter for OAuth2WebServerFlow is '
+            'deprecated. Please use the prompt parameter instead.')
+
+        if approval_prompt == 'force':
+            logger.warning(
+                'approval_prompt="force" has been adjusted to '
+                'prompt="consent"')
+            params['prompt'] = 'consent'
+            del params['approval_prompt']
+
+    return params
+
+
+class OAuth2WebServerFlow(Flow):
+    """Does the Web Server Flow for OAuth 2.0.
+
+    OAuth2WebServerFlow objects may be safely pickled and unpickled.
+    """
+
+    @util.positional(4)
+    def __init__(self, client_id,
+                 client_secret=None,
+                 scope=None,
+                 redirect_uri=None,
+                 user_agent=None,
+                 auth_uri=oauth2client.GOOGLE_AUTH_URI,
+                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+                 login_hint=None,
+                 device_uri=oauth2client.GOOGLE_DEVICE_URI,
+                 token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
+                 authorization_header=None,
+                 **kwargs):
+        """Constructor for OAuth2WebServerFlow.
+
+        The kwargs argument is used to set extra query parameters on the
+        auth_uri. For example, the access_type and prompt
+        query parameters can be set via kwargs.
+
+        Args:
+            client_id: string, client identifier.
+            client_secret: string client secret.
+            scope: string or iterable of strings, scope(s) of the credentials
+                   being requested.
+            redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
+                          for a non-web-based application, or a URI that
+                          handles the callback from the authorization server.
+            user_agent: string, HTTP User-Agent to provide for this
+                        application.
+            auth_uri: string, URI for authorization endpoint. For convenience
+                      defaults to Google's endpoints but any OAuth 2.0 provider
+                      can be used.
+            token_uri: string, URI for token endpoint. For convenience
+                       defaults to Google's endpoints but any OAuth 2.0
+                       provider can be used.
+            revoke_uri: string, URI for revoke endpoint. For convenience
+                        defaults to Google's endpoints but any OAuth 2.0
+                        provider can be used.
+            login_hint: string, Either an email address or domain. Passing this
+                        hint will either pre-fill the email box on the sign-in
+                        form or select the proper multi-login session, thereby
+                        simplifying the login flow.
+            device_uri: string, URI for device authorization endpoint. For
+                        convenience defaults to Google's endpoints but any
+                        OAuth 2.0 provider can be used.
+            authorization_header: string, For use with OAuth 2.0 providers that
+                                  require a client to authenticate using a
+                                  header value instead of passing client_secret
+                                  in the POST body.
+            **kwargs: dict, The keyword arguments are all optional and required
+                      parameters for the OAuth calls.
+        """
+        # scope is a required argument, but to preserve backwards-compatibility
+        # we don't want to rearrange the positional arguments
+        if scope is None:
+            raise TypeError("The value of scope must not be None")
+        self.client_id = client_id
+        self.client_secret = client_secret
+        self.scope = util.scopes_to_string(scope)
+        self.redirect_uri = redirect_uri
+        self.login_hint = login_hint
+        self.user_agent = user_agent
+        self.auth_uri = auth_uri
+        self.token_uri = token_uri
+        self.revoke_uri = revoke_uri
+        self.device_uri = device_uri
+        self.token_info_uri = token_info_uri
+        self.authorization_header = authorization_header
+        self.params = _oauth2_web_server_flow_params(kwargs)
+
+    @util.positional(1)
+    def step1_get_authorize_url(self, redirect_uri=None, state=None):
+        """Returns a URI to redirect to the provider.
+
+        Args:
+            redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
+                          for a non-web-based application, or a URI that
+                          handles the callback from the authorization server.
+                          This parameter is deprecated, please move to passing
+                          the redirect_uri in via the constructor.
+            state: string, Opaque state string which is passed through the
+                   OAuth2 flow and returned to the client as a query parameter
+                   in the callback.
+
+        Returns:
+            A URI as a string to redirect the user to begin the authorization
+            flow.
+        """
+        if redirect_uri is not None:
+            logger.warning((
+                'The redirect_uri parameter for '
+                'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. '
+                'Please move to passing the redirect_uri in via the '
+                'constructor.'))
+            self.redirect_uri = redirect_uri
+
+        if self.redirect_uri is None:
+            raise ValueError('The value of redirect_uri must not be None.')
+
+        query_params = {
+            'client_id': self.client_id,
+            'redirect_uri': self.redirect_uri,
+            'scope': self.scope,
+        }
+        if state is not None:
+            query_params['state'] = state
+        if self.login_hint is not None:
+            query_params['login_hint'] = self.login_hint
+        query_params.update(self.params)
+        return _update_query_params(self.auth_uri, query_params)
+
+    @util.positional(1)
+    def step1_get_device_and_user_codes(self, http=None):
+        """Returns a user code and the verification URL where to enter it
+
+        Returns:
+            A user code as a string for the user to authorize the application
+            An URL as a string where the user has to enter the code
+        """
+        if self.device_uri is None:
+            raise ValueError('The value of device_uri must not be None.')
+
+        body = urllib.parse.urlencode({
+            'client_id': self.client_id,
+            'scope': self.scope,
+        })
+        headers = {
+            'content-type': 'application/x-www-form-urlencoded',
+        }
+
+        if self.user_agent is not None:
+            headers['user-agent'] = self.user_agent
+
+        if http is None:
+            http = transport.get_http_object()
+
+        resp, content = http.request(self.device_uri, method='POST', body=body,
+                                     headers=headers)
+        content = _helpers._from_bytes(content)
+        if resp.status == http_client.OK:
+            try:
+                flow_info = json.loads(content)
+            except ValueError as exc:
+                raise OAuth2DeviceCodeError(
+                    'Could not parse server response as JSON: "{0}", '
+                    'error: "{1}"'.format(content, exc))
+            return DeviceFlowInfo.FromResponse(flow_info)
+        else:
+            error_msg = 'Invalid response {0}.'.format(resp.status)
+            try:
+                error_dict = json.loads(content)
+                if 'error' in error_dict:
+                    error_msg += ' Error: {0}'.format(error_dict['error'])
+            except ValueError:
+                # Couldn't decode a JSON response, stick with the
+                # default message.
+                pass
+            raise OAuth2DeviceCodeError(error_msg)
+
+    @util.positional(2)
+    def step2_exchange(self, code=None, http=None, device_flow_info=None):
+        """Exchanges a code for OAuth2Credentials.
+
+        Args:
+            code: string, a dict-like object, or None. For a non-device
+                  flow, this is either the response code as a string, or a
+                  dictionary of query parameters to the redirect_uri. For a
+                  device flow, this should be None.
+            http: httplib2.Http, optional http instance to use when fetching
+                  credentials.
+            device_flow_info: DeviceFlowInfo, return value from step1 in the
+                              case of a device flow.
+
+        Returns:
+            An OAuth2Credentials object that can be used to authorize requests.
+
+        Raises:
+            FlowExchangeError: if a problem occurred exchanging the code for a
+                               refresh_token.
+            ValueError: if code and device_flow_info are both provided or both
+                        missing.
+        """
+        if code is None and device_flow_info is None:
+            raise ValueError('No code or device_flow_info provided.')
+        if code is not None and device_flow_info is not None:
+            raise ValueError('Cannot provide both code and device_flow_info.')
+
+        if code is None:
+            code = device_flow_info.device_code
+        elif not isinstance(code, (six.string_types, six.binary_type)):
+            if 'code' not in code:
+                raise FlowExchangeError(code.get(
+                    'error', 'No code was supplied in the query parameters.'))
+            code = code['code']
+
+        post_data = {
+            'client_id': self.client_id,
+            'code': code,
+            'scope': self.scope,
+        }
+        if self.client_secret is not None:
+            post_data['client_secret'] = self.client_secret
+        if device_flow_info is not None:
+            post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
+        else:
+            post_data['grant_type'] = 'authorization_code'
+            post_data['redirect_uri'] = self.redirect_uri
+        body = urllib.parse.urlencode(post_data)
+        headers = {
+            'content-type': 'application/x-www-form-urlencoded',
+        }
+        if self.authorization_header is not None:
+            headers['Authorization'] = self.authorization_header
+        if self.user_agent is not None:
+            headers['user-agent'] = self.user_agent
+
+        if http is None:
+            http = transport.get_http_object()
+
+        resp, content = http.request(self.token_uri, method='POST', body=body,
+                                     headers=headers)
+        d = _parse_exchange_token_response(content)
+        if resp.status == http_client.OK and 'access_token' in d:
+            access_token = d['access_token']
+            refresh_token = d.get('refresh_token', None)
+            if not refresh_token:
+                logger.info(
+                    'Received token response with no refresh_token. Consider '
+                    "reauthenticating with prompt='consent'.")
+            token_expiry = None
+            if 'expires_in' in d:
+                delta = datetime.timedelta(seconds=int(d['expires_in']))
+                token_expiry = delta + _UTCNOW()
+
+            extracted_id_token = None
+            if 'id_token' in d:
+                extracted_id_token = _extract_id_token(d['id_token'])
+
+            logger.info('Successfully retrieved access token')
+            return OAuth2Credentials(
+                access_token, self.client_id, self.client_secret,
+                refresh_token, token_expiry, self.token_uri, self.user_agent,
+                revoke_uri=self.revoke_uri, id_token=extracted_id_token,
+                token_response=d, scopes=self.scope,
+                token_info_uri=self.token_info_uri)
+        else:
+            logger.info('Failed to retrieve access token: %s', content)
+            if 'error' in d:
+                # you never know what those providers got to say
+                error_msg = (str(d['error']) +
+                             str(d.get('error_description', '')))
+            else:
+                error_msg = 'Invalid response: {0}.'.format(str(resp.status))
+            raise FlowExchangeError(error_msg)
+
+
+@util.positional(2)
+def flow_from_clientsecrets(filename, scope, redirect_uri=None,
+                            message=None, cache=None, login_hint=None,
+                            device_uri=None):
+    """Create a Flow from a clientsecrets file.
+
+    Will create the right kind of Flow based on the contents of the
+    clientsecrets file or will raise InvalidClientSecretsError for unknown
+    types of Flows.
+
+    Args:
+        filename: string, File name of client secrets.
+        scope: string or iterable of strings, scope(s) to request.
+        redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+                      a non-web-based application, or a URI that handles the
+                      callback from the authorization server.
+        message: string, A friendly string to display to the user if the
+                 clientsecrets file is missing or invalid. If message is
+                 provided then sys.exit will be called in the case of an error.
+                 If message in not provided then
+                 clientsecrets.InvalidClientSecretsError will be raised.
+        cache: An optional cache service client that implements get() and set()
+               methods. See clientsecrets.loadfile() for details.
+        login_hint: string, Either an email address or domain. Passing this
+                    hint will either pre-fill the email box on the sign-in form
+                    or select the proper multi-login session, thereby
+                    simplifying the login flow.
+        device_uri: string, URI for device authorization endpoint. For
+                    convenience defaults to Google's endpoints but any
+                    OAuth 2.0 provider can be used.
+
+    Returns:
+        A Flow object.
+
+    Raises:
+        UnknownClientSecretsFlowError: if the file describes an unknown kind of
+                                       Flow.
+        clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
+                                                 invalid.
+    """
+    try:
+        client_type, client_info = clientsecrets.loadfile(filename,
+                                                          cache=cache)
+        if client_type in (clientsecrets.TYPE_WEB,
+                           clientsecrets.TYPE_INSTALLED):
+            constructor_kwargs = {
+                'redirect_uri': redirect_uri,
+                'auth_uri': client_info['auth_uri'],
+                'token_uri': client_info['token_uri'],
+                'login_hint': login_hint,
+            }
+            revoke_uri = client_info.get('revoke_uri')
+            if revoke_uri is not None:
+                constructor_kwargs['revoke_uri'] = revoke_uri
+            if device_uri is not None:
+                constructor_kwargs['device_uri'] = device_uri
+            return OAuth2WebServerFlow(
+                client_info['client_id'], client_info['client_secret'],
+                scope, **constructor_kwargs)
+
+    except clientsecrets.InvalidClientSecretsError as e:
+        if message is not None:
+            if e.args:
+                message = ('The client secrets were invalid: '
+                           '\n{0}\n{1}'.format(e, message))
+            sys.exit(message)
+        else:
+            raise
+    else:
+        raise UnknownClientSecretsFlowError(
+            'This OAuth 2.0 flow is unsupported: {0!r}'.format(client_type))
diff --git a/oauth2client/clientsecrets.py b/oauth2client/clientsecrets.py
new file mode 100644
index 0000000..4b43e66
--- /dev/null
+++ b/oauth2client/clientsecrets.py
@@ -0,0 +1,174 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Utilities for reading OAuth 2.0 client secret files.
+
+A client_secrets.json file contains all the information needed to interact with
+an OAuth 2.0 protected service.
+"""
+
+import json
+
+import six
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+# Properties that make a client_secrets.json file valid.
+TYPE_WEB = 'web'
+TYPE_INSTALLED = 'installed'
+
+VALID_CLIENT = {
+    TYPE_WEB: {
+        'required': [
+            'client_id',
+            'client_secret',
+            'redirect_uris',
+            'auth_uri',
+            'token_uri',
+        ],
+        'string': [
+            'client_id',
+            'client_secret',
+        ],
+    },
+    TYPE_INSTALLED: {
+        'required': [
+            'client_id',
+            'client_secret',
+            'redirect_uris',
+            'auth_uri',
+            'token_uri',
+        ],
+        'string': [
+            'client_id',
+            'client_secret',
+        ],
+    },
+}
+
+
+class Error(Exception):
+    """Base error for this module."""
+
+
+class InvalidClientSecretsError(Error):
+    """Format of ClientSecrets file is invalid."""
+
+
+def _validate_clientsecrets(clientsecrets_dict):
+    """Validate parsed client secrets from a file.
+
+    Args:
+        clientsecrets_dict: dict, a dictionary holding the client secrets.
+
+    Returns:
+        tuple, a string of the client type and the information parsed
+        from the file.
+    """
+    _INVALID_FILE_FORMAT_MSG = (
+        'Invalid file format. See '
+        'https://developers.google.com/api-client-library/'
+        'python/guide/aaa_client_secrets')
+
+    if clientsecrets_dict is None:
+        raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
+    try:
+        (client_type, client_info), = clientsecrets_dict.items()
+    except (ValueError, AttributeError):
+        raise InvalidClientSecretsError(
+            _INVALID_FILE_FORMAT_MSG + ' '
+            'Expected a JSON object with a single property for a "web" or '
+            '"installed" application')
+
+    if client_type not in VALID_CLIENT:
+        raise InvalidClientSecretsError(
+            'Unknown client type: {0}.'.format(client_type))
+
+    for prop_name in VALID_CLIENT[client_type]['required']:
+        if prop_name not in client_info:
+            raise InvalidClientSecretsError(
+                'Missing property "{0}" in a client type of "{1}".'.format(
+                    prop_name, client_type))
+    for prop_name in VALID_CLIENT[client_type]['string']:
+        if client_info[prop_name].startswith('[['):
+            raise InvalidClientSecretsError(
+                'Property "{0}" is not configured.'.format(prop_name))
+    return client_type, client_info
+
+
+def load(fp):
+    obj = json.load(fp)
+    return _validate_clientsecrets(obj)
+
+
+def loads(s):
+    obj = json.loads(s)
+    return _validate_clientsecrets(obj)
+
+
+def _loadfile(filename):
+    try:
+        with open(filename, 'r') as fp:
+            obj = json.load(fp)
+    except IOError as exc:
+        raise InvalidClientSecretsError('Error opening file', exc.filename,
+                                        exc.strerror, exc.errno)
+    return _validate_clientsecrets(obj)
+
+
+def loadfile(filename, cache=None):
+    """Loading of client_secrets JSON file, optionally backed by a cache.
+
+    Typical cache storage would be App Engine memcache service,
+    but you can pass in any other cache client that implements
+    these methods:
+
+    * ``get(key, namespace=ns)``
+    * ``set(key, value, namespace=ns)``
+
+    Usage::
+
+        # without caching
+        client_type, client_info = loadfile('secrets.json')
+        # using App Engine memcache service
+        from google.appengine.api import memcache
+        client_type, client_info = loadfile('secrets.json', cache=memcache)
+
+    Args:
+        filename: string, Path to a client_secrets.json file on a filesystem.
+        cache: An optional cache service client that implements get() and set()
+        methods. If not specified, the file is always being loaded from
+                 a filesystem.
+
+    Raises:
+        InvalidClientSecretsError: In case of a validation error or some
+                                   I/O failure. Can happen only on cache miss.
+
+    Returns:
+        (client_type, client_info) tuple, as _loadfile() normally would.
+        JSON contents is validated only during first load. Cache hits are not
+        validated.
+    """
+    _SECRET_NAMESPACE = 'oauth2client:secrets#ns'
+
+    if not cache:
+        return _loadfile(filename)
+
+    obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
+    if obj is None:
+        client_type, client_info = _loadfile(filename)
+        obj = {client_type: client_info}
+        cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
+
+    return next(six.iteritems(obj))
diff --git a/oauth2client/contrib/__init__.py b/oauth2client/contrib/__init__.py
new file mode 100644
index 0000000..ecfd06c
--- /dev/null
+++ b/oauth2client/contrib/__init__.py
@@ -0,0 +1,6 @@
+"""Contributed modules.
+
+Contrib contains modules that are not considered part of the core oauth2client
+library but provide additional functionality. These modules are intended to
+make it easier to use oauth2client.
+"""
diff --git a/oauth2client/contrib/_appengine_ndb.py b/oauth2client/contrib/_appengine_ndb.py
new file mode 100644
index 0000000..c863e8f
--- /dev/null
+++ b/oauth2client/contrib/_appengine_ndb.py
@@ -0,0 +1,163 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Google App Engine utilities helper.
+
+Classes that directly require App Engine's ndb library. Provided
+as a separate module in case of failure to import ndb while
+other App Engine libraries are present.
+"""
+
+import logging
+
+from google.appengine.ext import ndb
+
+from oauth2client import client
+
+
+NDB_KEY = ndb.Key
+"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
+
+NDB_MODEL = ndb.Model
+"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SiteXsrfSecretKeyNDB(ndb.Model):
+    """NDB Model for storage for the sites XSRF secret key.
+
+    Since this model uses the same kind as SiteXsrfSecretKey, it can be
+    used interchangeably. This simply provides an NDB model for interacting
+    with the same data the DB model interacts with.
+
+    There should only be one instance stored of this model, the one used
+    for the site.
+    """
+    secret = ndb.StringProperty()
+
+    @classmethod
+    def _get_kind(cls):
+        """Return the kind name for this class."""
+        return 'SiteXsrfSecretKey'
+
+
+class FlowNDBProperty(ndb.PickleProperty):
+    """App Engine NDB datastore Property for Flow.
+
+    Serves the same purpose as the DB FlowProperty, but for NDB models.
+    Since PickleProperty inherits from BlobProperty, the underlying
+    representation of the data in the datastore will be the same as in the
+    DB case.
+
+    Utility property that allows easy storage and retrieval of an
+    oauth2client.Flow
+    """
+
+    def _validate(self, value):
+        """Validates a value as a proper Flow object.
+
+        Args:
+            value: A value to be set on the property.
+
+        Raises:
+            TypeError if the value is not an instance of Flow.
+        """
+        _LOGGER.info('validate: Got type %s', type(value))
+        if value is not None and not isinstance(value, client.Flow):
+            raise TypeError(
+                'Property {0} must be convertible to a flow '
+                'instance; received: {1}.'.format(self._name, value))
+
+
+class CredentialsNDBProperty(ndb.BlobProperty):
+    """App Engine NDB datastore Property for Credentials.
+
+    Serves the same purpose as the DB CredentialsProperty, but for NDB
+    models. Since CredentialsProperty stores data as a blob and this
+    inherits from BlobProperty, the data in the datastore will be the same
+    as in the DB case.
+
+    Utility property that allows easy storage and retrieval of Credentials
+    and subclasses.
+    """
+
+    def _validate(self, value):
+        """Validates a value as a proper credentials object.
+
+        Args:
+            value: A value to be set on the property.
+
+        Raises:
+            TypeError if the value is not an instance of Credentials.
+        """
+        _LOGGER.info('validate: Got type %s', type(value))
+        if value is not None and not isinstance(value, client.Credentials):
+            raise TypeError(
+                'Property {0} must be convertible to a credentials '
+                'instance; received: {1}.'.format(self._name, value))
+
+    def _to_base_type(self, value):
+        """Converts our validated value to a JSON serialized string.
+
+        Args:
+            value: A value to be set in the datastore.
+
+        Returns:
+            A JSON serialized version of the credential, else '' if value
+            is None.
+        """
+        if value is None:
+            return ''
+        else:
+            return value.to_json()
+
+    def _from_base_type(self, value):
+        """Converts our stored JSON string back to the desired type.
+
+        Args:
+            value: A value from the datastore to be converted to the
+                   desired type.
+
+        Returns:
+            A deserialized Credentials (or subclass) object, else None if
+            the value can't be parsed.
+        """
+        if not value:
+            return None
+        try:
+            # Uses the from_json method of the implied class of value
+            credentials = client.Credentials.new_from_json(value)
+        except ValueError:
+            credentials = None
+        return credentials
+
+
+class CredentialsNDBModel(ndb.Model):
+    """NDB Model for storage of OAuth 2.0 Credentials
+
+    Since this model uses the same kind as CredentialsModel and has a
+    property which can serialize and deserialize Credentials correctly, it
+    can be used interchangeably with a CredentialsModel to access, insert
+    and delete the same entities. This simply provides an NDB model for
+    interacting with the same data the DB model interacts with.
+
+    Storage of the model is keyed by the user.user_id().
+    """
+    credentials = CredentialsNDBProperty()
+
+    @classmethod
+    def _get_kind(cls):
+        """Return the kind name for this class."""
+        return 'CredentialsModel'
diff --git a/oauth2client/contrib/_fcntl_opener.py b/oauth2client/contrib/_fcntl_opener.py
new file mode 100644
index 0000000..ae6c85b
--- /dev/null
+++ b/oauth2client/contrib/_fcntl_opener.py
@@ -0,0 +1,81 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import errno
+import fcntl
+import time
+
+from oauth2client.contrib import locked_file
+
+
+class _FcntlOpener(locked_file._Opener):
+    """Open, lock, and unlock a file using fcntl.lockf."""
+
+    def open_and_lock(self, timeout, delay):
+        """Open the file and lock it.
+
+        Args:
+            timeout: float, How long to try to lock for.
+            delay: float, How long to wait between retries
+
+        Raises:
+            AlreadyLockedException: if the lock is already acquired.
+            IOError: if the open fails.
+            CredentialsFileSymbolicLinkError: if the file is a symbolic
+                                              link.
+        """
+        if self._locked:
+            raise locked_file.AlreadyLockedException(
+                'File {0} is already locked'.format(self._filename))
+        start_time = time.time()
+
+        locked_file.validate_file(self._filename)
+        try:
+            self._fh = open(self._filename, self._mode)
+        except IOError as e:
+            # If we can't access with _mode, try _fallback_mode and
+            # don't lock.
+            if e.errno in (errno.EPERM, errno.EACCES):
+                self._fh = open(self._filename, self._fallback_mode)
+                return
+
+        # We opened in _mode, try to lock the file.
+        while True:
+            try:
+                fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
+                self._locked = True
+                return
+            except IOError as e:
+                # If not retrying, then just pass on the error.
+                if timeout == 0:
+                    raise
+                if e.errno != errno.EACCES:
+                    raise
+                # We could not acquire the lock. Try again.
+                if (time.time() - start_time) >= timeout:
+                    locked_file.logger.warn('Could not lock %s in %s seconds',
+                                            self._filename, timeout)
+                    if self._fh:
+                        self._fh.close()
+                    self._fh = open(self._filename, self._fallback_mode)
+                    return
+                time.sleep(delay)
+
+    def unlock_and_close(self):
+        """Close and unlock the file using the fcntl.lockf primitive."""
+        if self._locked:
+            fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
+        self._locked = False
+        if self._fh:
+            self._fh.close()
diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py
new file mode 100644
index 0000000..10e6a69
--- /dev/null
+++ b/oauth2client/contrib/_metadata.py
@@ -0,0 +1,123 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Provides helper methods for talking to the Compute Engine metadata server.
+
+See https://cloud.google.com/compute/docs/metadata
+"""
+
+import datetime
+import json
+
+import httplib2
+from six.moves import http_client
+from six.moves.urllib import parse as urlparse
+
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import util
+
+
+METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
+METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
+
+
+def get(http_request, path, root=METADATA_ROOT, recursive=None):
+    """Fetch a resource from the metadata server.
+
+    Args:
+        path: A string indicating the resource to retrieve. For example,
+            'instance/service-accounts/defualt'
+        http_request: A callable that matches the method
+            signature of httplib2.Http.request. Used to make the request to the
+            metadataserver.
+        root: A string indicating the full path to the metadata server root.
+        recursive: A boolean indicating whether to do a recursive query of
+            metadata. See
+            https://cloud.google.com/compute/docs/metadata#aggcontents
+
+    Returns:
+        A dictionary if the metadata server returns JSON, otherwise a string.
+
+    Raises:
+        httplib2.Httplib2Error if an error corrured while retrieving metadata.
+    """
+    url = urlparse.urljoin(root, path)
+    url = util._add_query_parameter(url, 'recursive', recursive)
+
+    response, content = http_request(
+        url,
+        headers=METADATA_HEADERS
+    )
+
+    if response.status == http_client.OK:
+        decoded = _helpers._from_bytes(content)
+        if response['content-type'] == 'application/json':
+            return json.loads(decoded)
+        else:
+            return decoded
+    else:
+        raise httplib2.HttpLib2Error(
+            'Failed to retrieve {0} from the Google Compute Engine'
+            'metadata service. Response:\n{1}'.format(url, response))
+
+
+def get_service_account_info(http_request, service_account='default'):
+    """Get information about a service account from the metadata server.
+
+    Args:
+        service_account: An email specifying the service account for which to
+            look up information. Default will be information for the "default"
+            service account of the current compute engine instance.
+        http_request: A callable that matches the method
+            signature of httplib2.Http.request. Used to make the request to the
+            metadata server.
+    Returns:
+         A dictionary with information about the specified service account,
+         for example:
+
+            {
+                'email': '...',
+                'scopes': ['scope', ...],
+                'aliases': ['default', '...']
+            }
+    """
+    return get(
+        http_request,
+        'instance/service-accounts/{0}/'.format(service_account),
+        recursive=True)
+
+
+def get_token(http_request, service_account='default'):
+    """Fetch an oauth token for the
+
+    Args:
+        service_account: An email specifying the service account this token
+            should represent. Default will be a token for the "default" service
+            account of the current compute engine instance.
+        http_request: A callable that matches the method
+            signature of httplib2.Http.request. Used to make the request to the
+            metadataserver.
+
+    Returns:
+         A tuple of (access token, token expiration), where access token is the
+         access token as a string and token expiration is a datetime object
+         that indicates when the access token will expire.
+    """
+    token_json = get(
+        http_request,
+        'instance/service-accounts/{0}/token'.format(service_account))
+    token_expiry = client._UTCNOW() + datetime.timedelta(
+        seconds=token_json['expires_in'])
+    return token_json['access_token'], token_expiry
diff --git a/oauth2client/contrib/_win32_opener.py b/oauth2client/contrib/_win32_opener.py
new file mode 100644
index 0000000..34b4f48
--- /dev/null
+++ b/oauth2client/contrib/_win32_opener.py
@@ -0,0 +1,106 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import errno
+import time
+
+import pywintypes
+import win32con
+import win32file
+
+from oauth2client.contrib import locked_file
+
+
+class _Win32Opener(locked_file._Opener):
+    """Open, lock, and unlock a file using windows primitives."""
+
+    # Error #33:
+    #  'The process cannot access the file because another process'
+    FILE_IN_USE_ERROR = 33
+
+    # Error #158:
+    #  'The segment is already unlocked.'
+    FILE_ALREADY_UNLOCKED_ERROR = 158
+
+    def open_and_lock(self, timeout, delay):
+        """Open the file and lock it.
+
+        Args:
+            timeout: float, How long to try to lock for.
+            delay: float, How long to wait between retries
+
+        Raises:
+            AlreadyLockedException: if the lock is already acquired.
+            IOError: if the open fails.
+            CredentialsFileSymbolicLinkError: if the file is a symbolic
+                                              link.
+        """
+        if self._locked:
+            raise locked_file.AlreadyLockedException(
+                'File {0} is already locked'.format(self._filename))
+        start_time = time.time()
+
+        locked_file.validate_file(self._filename)
+        try:
+            self._fh = open(self._filename, self._mode)
+        except IOError as e:
+            # If we can't access with _mode, try _fallback_mode
+            # and don't lock.
+            if e.errno == errno.EACCES:
+                self._fh = open(self._filename, self._fallback_mode)
+                return
+
+        # We opened in _mode, try to lock the file.
+        while True:
+            try:
+                hfile = win32file._get_osfhandle(self._fh.fileno())
+                win32file.LockFileEx(
+                    hfile,
+                    (win32con.LOCKFILE_FAIL_IMMEDIATELY |
+                     win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
+                    pywintypes.OVERLAPPED())
+                self._locked = True
+                return
+            except pywintypes.error as e:
+                if timeout == 0:
+                    raise
+
+                # If the error is not that the file is already
+                # in use, raise.
+                if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
+                    raise
+
+                # We could not acquire the lock. Try again.
+                if (time.time() - start_time) >= timeout:
+                    locked_file.logger.warn('Could not lock %s in %s seconds',
+                                            self._filename, timeout)
+                    if self._fh:
+                        self._fh.close()
+                    self._fh = open(self._filename, self._fallback_mode)
+                    return
+                time.sleep(delay)
+
+    def unlock_and_close(self):
+        """Close and unlock the file using the win32 primitive."""
+        if self._locked:
+            try:
+                hfile = win32file._get_osfhandle(self._fh.fileno())
+                win32file.UnlockFileEx(hfile, 0, -0x10000,
+                                       pywintypes.OVERLAPPED())
+            except pywintypes.error as e:
+                if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
+                    raise
+        self._locked = False
+        if self._fh:
+            self._fh.close()
diff --git a/oauth2client/contrib/appengine.py b/oauth2client/contrib/appengine.py
new file mode 100644
index 0000000..661105e
--- /dev/null
+++ b/oauth2client/contrib/appengine.py
@@ -0,0 +1,913 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Utilities for Google App Engine
+
+Utilities for making it easier to use OAuth 2.0 on Google App Engine.
+"""
+
+import cgi
+import json
+import logging
+import os
+import pickle
+import threading
+
+from google.appengine.api import app_identity
+from google.appengine.api import memcache
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext.webapp.util import login_required
+import httplib2
+import webapp2 as webapp
+
+import oauth2client
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client import util
+from oauth2client.contrib import xsrfutil
+
+# This is a temporary fix for a Google internal issue.
+try:
+    from oauth2client.contrib import _appengine_ndb
+except ImportError:  # pragma: NO COVER
+    _appengine_ndb = None
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+logger = logging.getLogger(__name__)
+
+OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
+
+XSRF_MEMCACHE_ID = 'xsrf_secret_key'
+
+if _appengine_ndb is None:  # pragma: NO COVER
+    CredentialsNDBModel = None
+    CredentialsNDBProperty = None
+    FlowNDBProperty = None
+    _NDB_KEY = None
+    _NDB_MODEL = None
+    SiteXsrfSecretKeyNDB = None
+else:
+    CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
+    CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
+    FlowNDBProperty = _appengine_ndb.FlowNDBProperty
+    _NDB_KEY = _appengine_ndb.NDB_KEY
+    _NDB_MODEL = _appengine_ndb.NDB_MODEL
+    SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
+
+
+def _safe_html(s):
+    """Escape text to make it safe to display.
+
+    Args:
+        s: string, The text to escape.
+
+    Returns:
+        The escaped text as a string.
+    """
+    return cgi.escape(s, quote=1).replace("'", '&#39;')
+
+
+class SiteXsrfSecretKey(db.Model):
+    """Storage for the sites XSRF secret key.
+
+    There will only be one instance stored of this model, the one used for the
+    site.
+    """
+    secret = db.StringProperty()
+
+
+def _generate_new_xsrf_secret_key():
+    """Returns a random XSRF secret key."""
+    return os.urandom(16).encode("hex")
+
+
+def xsrf_secret_key():
+    """Return the secret key for use for XSRF protection.
+
+    If the Site entity does not have a secret key, this method will also create
+    one and persist it.
+
+    Returns:
+        The secret key.
+    """
+    secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
+    if not secret:
+        # Load the one and only instance of SiteXsrfSecretKey.
+        model = SiteXsrfSecretKey.get_or_insert(key_name='site')
+        if not model.secret:
+            model.secret = _generate_new_xsrf_secret_key()
+            model.put()
+        secret = model.secret
+        memcache.add(XSRF_MEMCACHE_ID, secret,
+                     namespace=OAUTH2CLIENT_NAMESPACE)
+
+    return str(secret)
+
+
+class AppAssertionCredentials(client.AssertionCredentials):
+    """Credentials object for App Engine Assertion Grants
+
+    This object will allow an App Engine application to identify itself to
+    Google and other OAuth 2.0 servers that can verify assertions. It can be
+    used for the purpose of accessing data stored under an account assigned to
+    the App Engine application itself.
+
+    This credential does not require a flow to instantiate because it
+    represents a two legged flow, and therefore has all of the required
+    information to generate and refresh its own access tokens.
+    """
+
+    @util.positional(2)
+    def __init__(self, scope, **kwargs):
+        """Constructor for AppAssertionCredentials
+
+        Args:
+            scope: string or iterable of strings, scope(s) of the credentials
+                   being requested.
+            **kwargs: optional keyword args, including:
+            service_account_id: service account id of the application. If None
+                                or unspecified, the default service account for
+                                the app is used.
+        """
+        self.scope = util.scopes_to_string(scope)
+        self._kwargs = kwargs
+        self.service_account_id = kwargs.get('service_account_id', None)
+        self._service_account_email = None
+
+        # Assertion type is no longer used, but still in the
+        # parent class signature.
+        super(AppAssertionCredentials, self).__init__(None)
+
+    @classmethod
+    def from_json(cls, json_data):
+        data = json.loads(json_data)
+        return AppAssertionCredentials(data['scope'])
+
+    def _refresh(self, http_request):
+        """Refreshes the access_token.
+
+        Since the underlying App Engine app_identity implementation does its
+        own caching we can skip all the storage hoops and just to a refresh
+        using the API.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          refresh request.
+
+        Raises:
+            AccessTokenRefreshError: When the refresh fails.
+        """
+        try:
+            scopes = self.scope.split()
+            (token, _) = app_identity.get_access_token(
+                scopes, service_account_id=self.service_account_id)
+        except app_identity.Error as e:
+            raise client.AccessTokenRefreshError(str(e))
+        self.access_token = token
+
+    @property
+    def serialization_data(self):
+        raise NotImplementedError('Cannot serialize credentials '
+                                  'for Google App Engine.')
+
+    def create_scoped_required(self):
+        return not self.scope
+
+    def create_scoped(self, scopes):
+        return AppAssertionCredentials(scopes, **self._kwargs)
+
+    def sign_blob(self, blob):
+        """Cryptographically sign a blob (of bytes).
+
+        Implements abstract method
+        :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
+
+        Args:
+            blob: bytes, Message to be signed.
+
+        Returns:
+            tuple, A pair of the private key ID used to sign the blob and
+            the signed contents.
+        """
+        return app_identity.sign_blob(blob)
+
+    @property
+    def service_account_email(self):
+        """Get the email for the current service account.
+
+        Returns:
+            string, The email associated with the Google App Engine
+            service account.
+        """
+        if self._service_account_email is None:
+            self._service_account_email = (
+                app_identity.get_service_account_name())
+        return self._service_account_email
+
+
+class FlowProperty(db.Property):
+    """App Engine datastore Property for Flow.
+
+    Utility property that allows easy storage and retrieval of an
+    oauth2client.Flow
+    """
+
+    # Tell what the user type is.
+    data_type = client.Flow
+
+    # For writing to datastore.
+    def get_value_for_datastore(self, model_instance):
+        flow = super(FlowProperty, self).get_value_for_datastore(
+            model_instance)
+        return db.Blob(pickle.dumps(flow))
+
+    # For reading from datastore.
+    def make_value_from_datastore(self, value):
+        if value is None:
+            return None
+        return pickle.loads(value)
+
+    def validate(self, value):
+        if value is not None and not isinstance(value, client.Flow):
+            raise db.BadValueError(
+                'Property {0} must be convertible '
+                'to a FlowThreeLegged instance ({1})'.format(self.name, value))
+        return super(FlowProperty, self).validate(value)
+
+    def empty(self, value):
+        return not value
+
+
+class CredentialsProperty(db.Property):
+    """App Engine datastore Property for Credentials.
+
+    Utility property that allows easy storage and retrieval of
+    oauth2client.Credentials
+    """
+
+    # Tell what the user type is.
+    data_type = client.Credentials
+
+    # For writing to datastore.
+    def get_value_for_datastore(self, model_instance):
+        logger.info("get: Got type " + str(type(model_instance)))
+        cred = super(CredentialsProperty, self).get_value_for_datastore(
+            model_instance)
+        if cred is None:
+            cred = ''
+        else:
+            cred = cred.to_json()
+        return db.Blob(cred)
+
+    # For reading from datastore.
+    def make_value_from_datastore(self, value):
+        logger.info("make: Got type " + str(type(value)))
+        if value is None:
+            return None
+        if len(value) == 0:
+            return None
+        try:
+            credentials = client.Credentials.new_from_json(value)
+        except ValueError:
+            credentials = None
+        return credentials
+
+    def validate(self, value):
+        value = super(CredentialsProperty, self).validate(value)
+        logger.info("validate: Got type " + str(type(value)))
+        if value is not None and not isinstance(value, client.Credentials):
+            raise db.BadValueError(
+                'Property {0} must be convertible '
+                'to a Credentials instance ({1})'.format(self.name, value))
+        return value
+
+
+class StorageByKeyName(client.Storage):
+    """Store and retrieve a credential to and from the App Engine datastore.
+
+    This Storage helper presumes the Credentials have been stored as a
+    CredentialsProperty or CredentialsNDBProperty on a datastore model class,
+    and that entities are stored by key_name.
+    """
+
+    @util.positional(4)
+    def __init__(self, model, key_name, property_name, cache=None, user=None):
+        """Constructor for Storage.
+
+        Args:
+            model: db.Model or ndb.Model, model class
+            key_name: string, key name for the entity that has the credentials
+            property_name: string, name of the property that is a
+                           CredentialsProperty or CredentialsNDBProperty.
+            cache: memcache, a write-through cache to put in front of the
+                   datastore. If the model you are using is an NDB model, using
+                   a cache will be redundant since the model uses an instance
+                   cache and memcache for you.
+            user: users.User object, optional. Can be used to grab user ID as a
+                  key_name if no key name is specified.
+        """
+        super(StorageByKeyName, self).__init__()
+
+        if key_name is None:
+            if user is None:
+                raise ValueError('StorageByKeyName called with no '
+                                 'key name or user.')
+            key_name = user.user_id()
+
+        self._model = model
+        self._key_name = key_name
+        self._property_name = property_name
+        self._cache = cache
+
+    def _is_ndb(self):
+        """Determine whether the model of the instance is an NDB model.
+
+        Returns:
+            Boolean indicating whether or not the model is an NDB or DB model.
+        """
+        # issubclass will fail if one of the arguments is not a class, only
+        # need worry about new-style classes since ndb and db models are
+        # new-style
+        if isinstance(self._model, type):
+            if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
+                return True
+            elif issubclass(self._model, db.Model):
+                return False
+
+        raise TypeError(
+            'Model class not an NDB or DB model: {0}.'.format(self._model))
+
+    def _get_entity(self):
+        """Retrieve entity from datastore.
+
+        Uses a different model method for db or ndb models.
+
+        Returns:
+            Instance of the model corresponding to the current storage object
+            and stored using the key name of the storage object.
+        """
+        if self._is_ndb():
+            return self._model.get_by_id(self._key_name)
+        else:
+            return self._model.get_by_key_name(self._key_name)
+
+    def _delete_entity(self):
+        """Delete entity from datastore.
+
+        Attempts to delete using the key_name stored on the object, whether or
+        not the given key is in the datastore.
+        """
+        if self._is_ndb():
+            _NDB_KEY(self._model, self._key_name).delete()
+        else:
+            entity_key = db.Key.from_path(self._model.kind(), self._key_name)
+            db.delete(entity_key)
+
+    @db.non_transactional(allow_existing=True)
+    def locked_get(self):
+        """Retrieve Credential from datastore.
+
+        Returns:
+            oauth2client.Credentials
+        """
+        credentials = None
+        if self._cache:
+            json = self._cache.get(self._key_name)
+            if json:
+                credentials = client.Credentials.new_from_json(json)
+        if credentials is None:
+            entity = self._get_entity()
+            if entity is not None:
+                credentials = getattr(entity, self._property_name)
+                if self._cache:
+                    self._cache.set(self._key_name, credentials.to_json())
+
+        if credentials and hasattr(credentials, 'set_store'):
+            credentials.set_store(self)
+        return credentials
+
+    @db.non_transactional(allow_existing=True)
+    def locked_put(self, credentials):
+        """Write a Credentials to the datastore.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        entity = self._model.get_or_insert(self._key_name)
+        setattr(entity, self._property_name, credentials)
+        entity.put()
+        if self._cache:
+            self._cache.set(self._key_name, credentials.to_json())
+
+    @db.non_transactional(allow_existing=True)
+    def locked_delete(self):
+        """Delete Credential from datastore."""
+
+        if self._cache:
+            self._cache.delete(self._key_name)
+
+        self._delete_entity()
+
+
+class CredentialsModel(db.Model):
+    """Storage for OAuth 2.0 Credentials
+
+    Storage of the model is keyed by the user.user_id().
+    """
+    credentials = CredentialsProperty()
+
+
+def _build_state_value(request_handler, user):
+    """Composes the value for the 'state' parameter.
+
+    Packs the current request URI and an XSRF token into an opaque string that
+    can be passed to the authentication server via the 'state' parameter.
+
+    Args:
+        request_handler: webapp.RequestHandler, The request.
+        user: google.appengine.api.users.User, The current user.
+
+    Returns:
+        The state value as a string.
+    """
+    uri = request_handler.request.url
+    token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
+                                    action_id=str(uri))
+    return uri + ':' + token
+
+
+def _parse_state_value(state, user):
+    """Parse the value of the 'state' parameter.
+
+    Parses the value and validates the XSRF token in the state parameter.
+
+    Args:
+        state: string, The value of the state parameter.
+        user: google.appengine.api.users.User, The current user.
+
+    Returns:
+        The redirect URI, or None if XSRF token is not valid.
+    """
+    uri, token = state.rsplit(':', 1)
+    if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
+                               action_id=uri):
+        return uri
+    else:
+        return None
+
+
+class OAuth2Decorator(object):
+    """Utility for making OAuth 2.0 easier.
+
+    Instantiate and then use with oauth_required or oauth_aware
+    as decorators on webapp.RequestHandler methods.
+
+    ::
+
+        decorator = OAuth2Decorator(
+            client_id='837...ent.com',
+            client_secret='Qh...wwI',
+            scope='https://www.googleapis.com/auth/plus')
+
+        class MainHandler(webapp.RequestHandler):
+            @decorator.oauth_required
+            def get(self):
+                http = decorator.http()
+                # http is authorized with the user's Credentials and can be
+                # used in API calls
+
+    """
+
+    def set_credentials(self, credentials):
+        self._tls.credentials = credentials
+
+    def get_credentials(self):
+        """A thread local Credentials object.
+
+        Returns:
+            A client.Credentials object, or None if credentials hasn't been set
+            in this thread yet, which may happen when calling has_credentials
+            inside oauth_aware.
+        """
+        return getattr(self._tls, 'credentials', None)
+
+    credentials = property(get_credentials, set_credentials)
+
+    def set_flow(self, flow):
+        self._tls.flow = flow
+
+    def get_flow(self):
+        """A thread local Flow object.
+
+        Returns:
+            A credentials.Flow object, or None if the flow hasn't been set in
+            this thread yet, which happens in _create_flow() since Flows are
+            created lazily.
+        """
+        return getattr(self._tls, 'flow', None)
+
+    flow = property(get_flow, set_flow)
+
+    @util.positional(4)
+    def __init__(self, client_id, client_secret, scope,
+                 auth_uri=oauth2client.GOOGLE_AUTH_URI,
+                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+                 user_agent=None,
+                 message=None,
+                 callback_path='/oauth2callback',
+                 token_response_param=None,
+                 _storage_class=StorageByKeyName,
+                 _credentials_class=CredentialsModel,
+                 _credentials_property_name='credentials',
+                 **kwargs):
+        """Constructor for OAuth2Decorator
+
+        Args:
+            client_id: string, client identifier.
+            client_secret: string client secret.
+            scope: string or iterable of strings, scope(s) of the credentials
+                   being requested.
+            auth_uri: string, URI for authorization endpoint. For convenience
+                      defaults to Google's endpoints but any OAuth 2.0 provider
+                      can be used.
+            token_uri: string, URI for token endpoint. For convenience defaults
+                       to Google's endpoints but any OAuth 2.0 provider can be
+                       used.
+            revoke_uri: string, URI for revoke endpoint. For convenience
+                        defaults to Google's endpoints but any OAuth 2.0
+                        provider can be used.
+            user_agent: string, User agent of your application, default to
+                        None.
+            message: Message to display if there are problems with the
+                     OAuth 2.0 configuration. The message may contain HTML and
+                     will be presented on the web interface for any method that
+                     uses the decorator.
+            callback_path: string, The absolute path to use as the callback
+                           URI. Note that this must match up with the URI given
+                           when registering the application in the APIs
+                           Console.
+            token_response_param: string. If provided, the full JSON response
+                                  to the access token request will be encoded
+                                  and included in this query parameter in the
+                                  callback URI. This is useful with providers
+                                  (e.g. wordpress.com) that include extra
+                                  fields that the client may want.
+            _storage_class: "Protected" keyword argument not typically provided
+                            to this constructor. A storage class to aid in
+                            storing a Credentials object for a user in the
+                            datastore. Defaults to StorageByKeyName.
+            _credentials_class: "Protected" keyword argument not typically
+                                provided to this constructor. A db or ndb Model
+                                class to hold credentials. Defaults to
+                                CredentialsModel.
+            _credentials_property_name: "Protected" keyword argument not
+                                        typically provided to this constructor.
+                                        A string indicating the name of the
+                                        field on the _credentials_class where a
+                                        Credentials object will be stored.
+                                        Defaults to 'credentials'.
+            **kwargs: dict, Keyword arguments are passed along as kwargs to
+                      the OAuth2WebServerFlow constructor.
+        """
+        self._tls = threading.local()
+        self.flow = None
+        self.credentials = None
+        self._client_id = client_id
+        self._client_secret = client_secret
+        self._scope = util.scopes_to_string(scope)
+        self._auth_uri = auth_uri
+        self._token_uri = token_uri
+        self._revoke_uri = revoke_uri
+        self._user_agent = user_agent
+        self._kwargs = kwargs
+        self._message = message
+        self._in_error = False
+        self._callback_path = callback_path
+        self._token_response_param = token_response_param
+        self._storage_class = _storage_class
+        self._credentials_class = _credentials_class
+        self._credentials_property_name = _credentials_property_name
+
+    def _display_error_message(self, request_handler):
+        request_handler.response.out.write('<html><body>')
+        request_handler.response.out.write(_safe_html(self._message))
+        request_handler.response.out.write('</body></html>')
+
+    def oauth_required(self, method):
+        """Decorator that starts the OAuth 2.0 dance.
+
+        Starts the OAuth dance for the logged in user if they haven't already
+        granted access for this application.
+
+        Args:
+            method: callable, to be decorated method of a webapp.RequestHandler
+                    instance.
+        """
+
+        def check_oauth(request_handler, *args, **kwargs):
+            if self._in_error:
+                self._display_error_message(request_handler)
+                return
+
+            user = users.get_current_user()
+            # Don't use @login_decorator as this could be used in a
+            # POST request.
+            if not user:
+                request_handler.redirect(users.create_login_url(
+                    request_handler.request.uri))
+                return
+
+            self._create_flow(request_handler)
+
+            # Store the request URI in 'state' so we can use it later
+            self.flow.params['state'] = _build_state_value(
+                request_handler, user)
+            self.credentials = self._storage_class(
+                self._credentials_class, None,
+                self._credentials_property_name, user=user).get()
+
+            if not self.has_credentials():
+                return request_handler.redirect(self.authorize_url())
+            try:
+                resp = method(request_handler, *args, **kwargs)
+            except client.AccessTokenRefreshError:
+                return request_handler.redirect(self.authorize_url())
+            finally:
+                self.credentials = None
+            return resp
+
+        return check_oauth
+
+    def _create_flow(self, request_handler):
+        """Create the Flow object.
+
+        The Flow is calculated lazily since we don't know where this app is
+        running until it receives a request, at which point redirect_uri can be
+        calculated and then the Flow object can be constructed.
+
+        Args:
+            request_handler: webapp.RequestHandler, the request handler.
+        """
+        if self.flow is None:
+            redirect_uri = request_handler.request.relative_url(
+                self._callback_path)  # Usually /oauth2callback
+            self.flow = client.OAuth2WebServerFlow(
+                self._client_id, self._client_secret, self._scope,
+                redirect_uri=redirect_uri, user_agent=self._user_agent,
+                auth_uri=self._auth_uri, token_uri=self._token_uri,
+                revoke_uri=self._revoke_uri, **self._kwargs)
+
+    def oauth_aware(self, method):
+        """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
+
+        Does all the setup for the OAuth dance, but doesn't initiate it.
+        This decorator is useful if you want to create a page that knows
+        whether or not the user has granted access to this application.
+        From within a method decorated with @oauth_aware the has_credentials()
+        and authorize_url() methods can be called.
+
+        Args:
+            method: callable, to be decorated method of a webapp.RequestHandler
+                    instance.
+        """
+
+        def setup_oauth(request_handler, *args, **kwargs):
+            if self._in_error:
+                self._display_error_message(request_handler)
+                return
+
+            user = users.get_current_user()
+            # Don't use @login_decorator as this could be used in a
+            # POST request.
+            if not user:
+                request_handler.redirect(users.create_login_url(
+                    request_handler.request.uri))
+                return
+
+            self._create_flow(request_handler)
+
+            self.flow.params['state'] = _build_state_value(request_handler,
+                                                           user)
+            self.credentials = self._storage_class(
+                self._credentials_class, None,
+                self._credentials_property_name, user=user).get()
+            try:
+                resp = method(request_handler, *args, **kwargs)
+            finally:
+                self.credentials = None
+            return resp
+        return setup_oauth
+
+    def has_credentials(self):
+        """True if for the logged in user there are valid access Credentials.
+
+        Must only be called from with a webapp.RequestHandler subclassed method
+        that had been decorated with either @oauth_required or @oauth_aware.
+        """
+        return self.credentials is not None and not self.credentials.invalid
+
+    def authorize_url(self):
+        """Returns the URL to start the OAuth dance.
+
+        Must only be called from with a webapp.RequestHandler subclassed method
+        that had been decorated with either @oauth_required or @oauth_aware.
+        """
+        url = self.flow.step1_get_authorize_url()
+        return str(url)
+
+    def http(self, *args, **kwargs):
+        """Returns an authorized http instance.
+
+        Must only be called from within an @oauth_required decorated method, or
+        from within an @oauth_aware decorated method where has_credentials()
+        returns True.
+
+        Args:
+            *args: Positional arguments passed to httplib2.Http constructor.
+            **kwargs: Positional arguments passed to httplib2.Http constructor.
+        """
+        return self.credentials.authorize(httplib2.Http(*args, **kwargs))
+
+    @property
+    def callback_path(self):
+        """The absolute path where the callback will occur.
+
+        Note this is the absolute path, not the absolute URI, that will be
+        calculated by the decorator at runtime. See callback_handler() for how
+        this should be used.
+
+        Returns:
+            The callback path as a string.
+        """
+        return self._callback_path
+
+    def callback_handler(self):
+        """RequestHandler for the OAuth 2.0 redirect callback.
+
+        Usage::
+
+            app = webapp.WSGIApplication([
+                ('/index', MyIndexHandler),
+                ...,
+                (decorator.callback_path, decorator.callback_handler())
+            ])
+
+        Returns:
+            A webapp.RequestHandler that handles the redirect back from the
+            server during the OAuth 2.0 dance.
+        """
+        decorator = self
+
+        class OAuth2Handler(webapp.RequestHandler):
+            """Handler for the redirect_uri of the OAuth 2.0 dance."""
+
+            @login_required
+            def get(self):
+                error = self.request.get('error')
+                if error:
+                    errormsg = self.request.get('error_description', error)
+                    self.response.out.write(
+                        'The authorization request failed: {0}'.format(
+                            _safe_html(errormsg)))
+                else:
+                    user = users.get_current_user()
+                    decorator._create_flow(self)
+                    credentials = decorator.flow.step2_exchange(
+                        self.request.params)
+                    decorator._storage_class(
+                        decorator._credentials_class, None,
+                        decorator._credentials_property_name,
+                        user=user).put(credentials)
+                    redirect_uri = _parse_state_value(
+                        str(self.request.get('state')), user)
+                    if redirect_uri is None:
+                        self.response.out.write(
+                            'The authorization request failed')
+                        return
+
+                    if (decorator._token_response_param and
+                            credentials.token_response):
+                        resp_json = json.dumps(credentials.token_response)
+                        redirect_uri = util._add_query_parameter(
+                            redirect_uri, decorator._token_response_param,
+                            resp_json)
+
+                    self.redirect(redirect_uri)
+
+        return OAuth2Handler
+
+    def callback_application(self):
+        """WSGI application for handling the OAuth 2.0 redirect callback.
+
+        If you need finer grained control use `callback_handler` which returns
+        just the webapp.RequestHandler.
+
+        Returns:
+            A webapp.WSGIApplication that handles the redirect back from the
+            server during the OAuth 2.0 dance.
+        """
+        return webapp.WSGIApplication([
+            (self.callback_path, self.callback_handler())
+        ])
+
+
+class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
+    """An OAuth2Decorator that builds from a clientsecrets file.
+
+    Uses a clientsecrets file as the source for all the information when
+    constructing an OAuth2Decorator.
+
+    ::
+
+        decorator = OAuth2DecoratorFromClientSecrets(
+            os.path.join(os.path.dirname(__file__), 'client_secrets.json')
+            scope='https://www.googleapis.com/auth/plus')
+
+        class MainHandler(webapp.RequestHandler):
+            @decorator.oauth_required
+            def get(self):
+                http = decorator.http()
+                # http is authorized with the user's Credentials and can be
+                # used in API calls
+
+    """
+
+    @util.positional(3)
+    def __init__(self, filename, scope, message=None, cache=None, **kwargs):
+        """Constructor
+
+        Args:
+            filename: string, File name of client secrets.
+            scope: string or iterable of strings, scope(s) of the credentials
+                   being requested.
+            message: string, A friendly string to display to the user if the
+                     clientsecrets file is missing or invalid. The message may
+                     contain HTML and will be presented on the web interface
+                     for any method that uses the decorator.
+            cache: An optional cache service client that implements get() and
+                   set()
+            methods. See clientsecrets.loadfile() for details.
+            **kwargs: dict, Keyword arguments are passed along as kwargs to
+                      the OAuth2WebServerFlow constructor.
+        """
+        client_type, client_info = clientsecrets.loadfile(filename,
+                                                          cache=cache)
+        if client_type not in (clientsecrets.TYPE_WEB,
+                               clientsecrets.TYPE_INSTALLED):
+            raise clientsecrets.InvalidClientSecretsError(
+                "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
+
+        constructor_kwargs = dict(kwargs)
+        constructor_kwargs.update({
+            'auth_uri': client_info['auth_uri'],
+            'token_uri': client_info['token_uri'],
+            'message': message,
+        })
+        revoke_uri = client_info.get('revoke_uri')
+        if revoke_uri is not None:
+            constructor_kwargs['revoke_uri'] = revoke_uri
+        super(OAuth2DecoratorFromClientSecrets, self).__init__(
+            client_info['client_id'], client_info['client_secret'],
+            scope, **constructor_kwargs)
+        if message is not None:
+            self._message = message
+        else:
+            self._message = 'Please configure your application for OAuth 2.0.'
+
+
+@util.positional(2)
+def oauth2decorator_from_clientsecrets(filename, scope,
+                                       message=None, cache=None):
+    """Creates an OAuth2Decorator populated from a clientsecrets file.
+
+    Args:
+        filename: string, File name of client secrets.
+        scope: string or list of strings, scope(s) of the credentials being
+               requested.
+        message: string, A friendly string to display to the user if the
+                 clientsecrets file is missing or invalid. The message may
+                 contain HTML and will be presented on the web interface for
+                 any method that uses the decorator.
+        cache: An optional cache service client that implements get() and set()
+               methods. See clientsecrets.loadfile() for details.
+
+    Returns: An OAuth2Decorator
+    """
+    return OAuth2DecoratorFromClientSecrets(filename, scope,
+                                            message=message, cache=cache)
diff --git a/oauth2client/contrib/devshell.py b/oauth2client/contrib/devshell.py
new file mode 100644
index 0000000..b8bb978
--- /dev/null
+++ b/oauth2client/contrib/devshell.py
@@ -0,0 +1,146 @@
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# 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 utitilies for Google Developer Shell environment."""
+
+import datetime
+import json
+import os
+import socket
+
+from oauth2client import _helpers
+from oauth2client import client
+
+DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
+
+
+class Error(Exception):
+    """Errors for this module."""
+    pass
+
+
+class CommunicationError(Error):
+    """Errors for communication with the Developer Shell server."""
+
+
+class NoDevshellServer(Error):
+    """Error when no Developer Shell server can be contacted."""
+
+# The request for credential information to the Developer Shell client socket
+# is always an empty PBLite-formatted JSON object, so just define it as a
+# constant.
+CREDENTIAL_INFO_REQUEST_JSON = '[]'
+
+
+class CredentialInfoResponse(object):
+    """Credential information response from Developer Shell server.
+
+    The credential information response from Developer Shell socket is a
+    PBLite-formatted JSON array with fields encoded by their index in the
+    array:
+
+    * Index 0 - user email
+    * Index 1 - default project ID. None if the project context is not known.
+    * Index 2 - OAuth2 access token. None if there is no valid auth context.
+    * Index 3 - Seconds until the access token expires. None if not present.
+    """
+
+    def __init__(self, json_string):
+        """Initialize the response data from JSON PBLite array."""
+        pbl = json.loads(json_string)
+        if not isinstance(pbl, list):
+            raise ValueError('Not a list: ' + str(pbl))
+        pbl_len = len(pbl)
+        self.user_email = pbl[0] if pbl_len > 0 else None
+        self.project_id = pbl[1] if pbl_len > 1 else None
+        self.access_token = pbl[2] if pbl_len > 2 else None
+        self.expires_in = pbl[3] if pbl_len > 3 else None
+
+
+def _SendRecv():
+    """Communicate with the Developer Shell server socket."""
+
+    port = int(os.getenv(DEVSHELL_ENV, 0))
+    if port == 0:
+        raise NoDevshellServer()
+
+    sock = socket.socket()
+    sock.connect(('localhost', port))
+
+    data = CREDENTIAL_INFO_REQUEST_JSON
+    msg = '{0}\n{1}'.format(len(data), data)
+    sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
+
+    header = sock.recv(6).decode()
+    if '\n' not in header:
+        raise CommunicationError('saw no newline in the first 6 bytes')
+    len_str, json_str = header.split('\n', 1)
+    to_read = int(len_str) - len(json_str)
+    if to_read > 0:
+        json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
+
+    return CredentialInfoResponse(json_str)
+
+
+class DevshellCredentials(client.GoogleCredentials):
+    """Credentials object for Google Developer Shell environment.
+
+    This object will allow a Google Developer Shell session to identify its
+    user to Google and other OAuth 2.0 servers that can verify assertions. It
+    can be used for the purpose of accessing data stored under the user
+    account.
+
+    This credential does not require a flow to instantiate because it
+    represents a two legged flow, and therefore has all of the required
+    information to generate and refresh its own access tokens.
+    """
+
+    def __init__(self, user_agent=None):
+        super(DevshellCredentials, self).__init__(
+            None,  # access_token, initialized below
+            None,  # client_id
+            None,  # client_secret
+            None,  # refresh_token
+            None,  # token_expiry
+            None,  # token_uri
+            user_agent)
+        self._refresh(None)
+
+    def _refresh(self, http_request):
+        self.devshell_response = _SendRecv()
+        self.access_token = self.devshell_response.access_token
+        expires_in = self.devshell_response.expires_in
+        if expires_in is not None:
+            delta = datetime.timedelta(seconds=expires_in)
+            self.token_expiry = client._UTCNOW() + delta
+        else:
+            self.token_expiry = None
+
+    @property
+    def user_email(self):
+        return self.devshell_response.user_email
+
+    @property
+    def project_id(self):
+        return self.devshell_response.project_id
+
+    @classmethod
+    def from_json(cls, json_data):
+        raise NotImplementedError(
+            'Cannot load Developer Shell credentials from JSON.')
+
+    @property
+    def serialization_data(self):
+        raise NotImplementedError(
+            'Cannot serialize Developer Shell credentials.')
diff --git a/oauth2client/contrib/dictionary_storage.py b/oauth2client/contrib/dictionary_storage.py
new file mode 100644
index 0000000..6ee333f
--- /dev/null
+++ b/oauth2client/contrib/dictionary_storage.py
@@ -0,0 +1,65 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Dictionary storage for OAuth2 Credentials."""
+
+from oauth2client import client
+
+
+class DictionaryStorage(client.Storage):
+    """Store and retrieve credentials to and from a dictionary-like object.
+
+    Args:
+        dictionary: A dictionary or dictionary-like object.
+        key: A string or other hashable. The credentials will be stored in
+             ``dictionary[key]``.
+        lock: An optional threading.Lock-like object. The lock will be
+              acquired before anything is written or read from the
+              dictionary.
+    """
+
+    def __init__(self, dictionary, key, lock=None):
+        """Construct a DictionaryStorage instance."""
+        super(DictionaryStorage, self).__init__(lock=lock)
+        self._dictionary = dictionary
+        self._key = key
+
+    def locked_get(self):
+        """Retrieve the credentials from the dictionary, if they exist.
+
+        Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
+        """
+        serialized = self._dictionary.get(self._key)
+
+        if serialized is None:
+            return None
+
+        credentials = client.OAuth2Credentials.from_json(serialized)
+        credentials.set_store(self)
+
+        return credentials
+
+    def locked_put(self, credentials):
+        """Save the credentials to the dictionary.
+
+        Args:
+            credentials: A :class:`oauth2client.client.OAuth2Credentials`
+                         instance.
+        """
+        serialized = credentials.to_json()
+        self._dictionary[self._key] = serialized
+
+    def locked_delete(self):
+        """Remove the credentials from the dictionary, if they exist."""
+        self._dictionary.pop(self._key, None)
diff --git a/oauth2client/contrib/django_util/__init__.py b/oauth2client/contrib/django_util/__init__.py
new file mode 100644
index 0000000..5449e32
--- /dev/null
+++ b/oauth2client/contrib/django_util/__init__.py
@@ -0,0 +1,477 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Utilities for the Django web framework.
+
+Provides Django views and helpers the make using the OAuth2 web server
+flow easier. It includes an ``oauth_required`` decorator to automatically
+ensure that user credentials are available, and an ``oauth_enabled`` decorator
+to check if the user has authorized, and helper shortcuts to create the
+authorization URL otherwise.
+
+There are two basic use cases supported. The first is using Google OAuth as the
+primary form of authentication, which is the simpler approach recommended
+for applications without their own user system.
+
+The second use case is adding Google OAuth credentials to an
+existing Django model containing a Django user field. Most of the
+configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in
+settings.py. See "Adding Credentials To An Existing Django User System" for
+usage differences.
+
+Only Django versions 1.8+ are supported.
+
+Configuration
+===============
+
+To configure, you'll need a set of OAuth2 web application credentials from
+`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
+
+Add the helper to your INSTALLED_APPS:
+
+.. code-block:: python
+   :caption: settings.py
+   :name: installed_apps
+
+    INSTALLED_APPS = (
+        # other apps
+        "django.contrib.sessions.middleware"
+        "oauth2client.contrib.django_util"
+    )
+
+This helper also requires the Django Session Middleware, so
+``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
+
+Add the client secrets created earlier to the settings. You can either
+specify the path to the credentials file in JSON format
+
+.. code-block:: python
+   :caption:  settings.py
+   :name: secrets_file
+
+   GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
+
+Or, directly configure the client Id and client secret.
+
+
+.. code-block:: python
+   :caption: settings.py
+   :name: secrets_config
+
+   GOOGLE_OAUTH2_CLIENT_ID=client-id-field
+   GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
+
+By default, the default scopes for the required decorator only contains the
+``email`` scopes. You can change that default in the settings.
+
+.. code-block:: python
+   :caption: settings.py
+   :name: scopes
+
+   GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
+
+By default, the decorators will add an `oauth` object to the Django request
+object, and include all of its state and helpers inside that object. If the
+`oauth` name conflicts with another usage, it can be changed
+
+.. code-block:: python
+   :caption: settings.py
+   :name: request_prefix
+
+   # changes request.oauth to request.google_oauth
+   GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
+
+Add the oauth2 routes to your application's urls.py urlpatterns.
+
+.. code-block:: python
+   :caption: urls.py
+   :name: urls
+
+   from oauth2client.contrib.django_util.site import urls as oauth2_urls
+
+   urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
+
+To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
+This creates a credentials object with an id_token, and allows you to create
+an `http` object to build service clients with. These are all attached to the
+request.oauth
+
+.. code-block:: python
+   :caption: views.py
+   :name: views_required
+
+   from oauth2client.contrib.django_util.decorators import oauth_required
+
+   @oauth_required
+   def requires_default_scopes(request):
+      email = request.oauth.credentials.id_token['email']
+      service = build(serviceName='calendar', version='v3',
+                    http=request.oauth.http,
+                   developerKey=API_KEY)
+      events = service.events().list(calendarId='primary').execute()['items']
+      return HttpResponse("email: {0} , calendar: {1}".format(
+                           email,str(events)))
+      return HttpResponse(
+          "email: {0} , calendar: {1}".format(email, str(events)))
+
+To make OAuth2 optional and provide an authorization link in your own views.
+
+.. code-block:: python
+   :caption: views.py
+   :name: views_enabled2
+
+   from oauth2client.contrib.django_util.decorators import oauth_enabled
+
+   @oauth_enabled
+   def optional_oauth2(request):
+       if request.oauth.has_credentials():
+           # this could be passed into a view
+           # request.oauth.http is also initialized
+           return HttpResponse("User email: {0}".format(
+               request.oauth.credentials.id_token['email']))
+       else:
+           return HttpResponse(
+               'Here is an OAuth Authorize link: <a href="{0}">Authorize'
+               '</a>'.format(request.oauth.get_authorize_redirect()))
+
+If a view needs a scope not included in the default scopes specified in
+the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
+and specify additional scopes in the decorator arguments.
+
+.. code-block:: python
+   :caption: views.py
+   :name: views_required_additional_scopes
+
+   @oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
+   def drive_required(request):
+       if request.oauth.has_credentials():
+           service = build(serviceName='drive', version='v2',
+                http=request.oauth.http,
+                developerKey=API_KEY)
+           events = service.files().list().execute()['items']
+           return HttpResponse(str(events))
+       else:
+           return HttpResponse(
+               'Here is an OAuth Authorize link: <a href="{0}">Authorize'
+               '</a>'.format(request.oauth.get_authorize_redirect()))
+
+
+To provide a callback on authorization being completed, use the
+oauth2_authorized signal:
+
+.. code-block:: python
+   :caption: views.py
+   :name: signals
+
+   from oauth2client.contrib.django_util.signals import oauth2_authorized
+
+   def test_callback(sender, request, credentials, **kwargs):
+       print("Authorization Signal Received {0}".format(
+               credentials.id_token['email']))
+
+   oauth2_authorized.connect(test_callback)
+
+Adding Credentials To An Existing Django User System
+=====================================================
+
+As an alternative to storing the credentials in the session, the helper
+can be configured to store the fields on a Django model. This might be useful
+if you need to use the credentials outside the context of a user request. It
+also prevents the need for a logged in user to repeat the OAuth flow when
+starting a new session.
+
+To use, change ``settings.py``
+
+.. code-block:: python
+   :caption:  settings.py
+   :name: storage_model_config
+
+   GOOGLE_OAUTH2_STORAGE_MODEL = {
+       'model': 'path.to.model.MyModel',
+       'user_property': 'user_id',
+       'credentials_property': 'credential'
+    }
+
+Where ``path.to.model`` class is the fully qualified name of a
+``django.db.model`` class containing a ``django.contrib.auth.models.User``
+field with the name specified by `user_property` and a
+:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name
+specified by `credentials_property`. For the sample configuration given,
+our model would look like
+
+.. code-block:: python
+   :caption: models.py
+   :name: storage_model_model
+
+   from django.contrib.auth.models import User
+   from oauth2client.contrib.django_util.models import CredentialsField
+
+   class MyModel(models.Model):
+       #  ... other fields here ...
+       user = models.OneToOneField(User)
+       credential = CredentialsField()
+"""
+
+import importlib
+
+import django.conf
+from django.core import exceptions
+from django.core import urlresolvers
+import httplib2
+from six.moves.urllib import parse
+
+from oauth2client import clientsecrets
+from oauth2client.contrib import dictionary_storage
+from oauth2client.contrib.django_util import storage
+
+GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
+GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
+
+
+def _load_client_secrets(filename):
+    """Loads client secrets from the given filename.
+
+    Args:
+        filename: The name of the file containing the JSON secret key.
+
+    Returns:
+        A 2-tuple, the first item containing the client id, and the second
+        item containing a client secret.
+    """
+    client_type, client_info = clientsecrets.loadfile(filename)
+
+    if client_type != clientsecrets.TYPE_WEB:
+        raise ValueError(
+            'The flow specified in {} is not supported, only the WEB flow '
+            'type  is supported.'.format(client_type))
+    return client_info['client_id'], client_info['client_secret']
+
+
+def _get_oauth2_client_id_and_secret(settings_instance):
+    """Initializes client id and client secret based on the settings.
+
+    Args:
+        settings_instance: An instance of ``django.conf.settings``.
+
+    Returns:
+        A 2-tuple, the first item is the client id and the second
+         item is the client secret.
+    """
+    secret_json = getattr(settings_instance,
+                          'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
+    if secret_json is not None:
+        return _load_client_secrets(secret_json)
+    else:
+        client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
+                            None)
+        client_secret = getattr(settings_instance,
+                                "GOOGLE_OAUTH2_CLIENT_SECRET", None)
+        if client_id is not None and client_secret is not None:
+            return client_id, client_secret
+        else:
+            raise exceptions.ImproperlyConfigured(
+                "Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
+                "both GOOGLE_OAUTH2_CLIENT_ID and "
+                "GOOGLE_OAUTH2_CLIENT_SECRET in settings.py")
+
+
+def _get_storage_model():
+    """This configures whether the credentials will be stored in the session
+    or the Django ORM based on the settings. By default, the credentials
+    will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL`
+    is found in the settings. Usually, the ORM storage is used to integrate
+    credentials into an existing Django user system.
+
+    Returns:
+        A tuple containing three strings, or None. If
+        ``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple
+        will contain the fully qualifed path of the `django.db.model`,
+        the name of the ``django.contrib.auth.models.User`` field on the
+        model, and the name of the
+        :class:`oauth2client.contrib.django_util.models.CredentialsField`
+        field on the model. If Django ORM storage is not configured,
+        this function returns None.
+    """
+    storage_model_settings = getattr(django.conf.settings,
+                                     'GOOGLE_OAUTH2_STORAGE_MODEL', None)
+    if storage_model_settings is not None:
+        return (storage_model_settings['model'],
+                storage_model_settings['user_property'],
+                storage_model_settings['credentials_property'])
+    else:
+        return None, None, None
+
+
+class OAuth2Settings(object):
+    """Initializes Django OAuth2 Helper Settings
+
+    This class loads the OAuth2 Settings from the Django settings, and then
+    provides those settings as attributes to the rest of the views and
+    decorators in the module.
+
+    Attributes:
+      scopes: A list of OAuth2 scopes that the decorators and views will use
+              as defaults.
+      request_prefix: The name of the attribute that the decorators use to
+                    attach the UserOAuth2 object to the Django request object.
+      client_id: The OAuth2 Client ID.
+      client_secret: The OAuth2 Client Secret.
+    """
+
+    def __init__(self, settings_instance):
+        self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
+                              GOOGLE_OAUTH2_DEFAULT_SCOPES)
+        self.request_prefix = getattr(settings_instance,
+                                      'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
+                                      GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
+        self.client_id, self.client_secret = \
+            _get_oauth2_client_id_and_secret(settings_instance)
+
+        if ('django.contrib.sessions.middleware.SessionMiddleware'
+           not in settings_instance.MIDDLEWARE_CLASSES):
+            raise exceptions.ImproperlyConfigured(
+                  'The Google OAuth2 Helper requires session middleware to '
+                  'be installed. Edit your MIDDLEWARE_CLASSES setting'
+                  ' to include \'django.contrib.sessions.middleware.'
+                  'SessionMiddleware\'.')
+        (self.storage_model, self.storage_model_user_property,
+         self.storage_model_credentials_property) = _get_storage_model()
+
+
+oauth2_settings = OAuth2Settings(django.conf.settings)
+
+_CREDENTIALS_KEY = 'google_oauth2_credentials'
+
+
+def get_storage(request):
+    """ Gets a Credentials storage object provided by the Django OAuth2 Helper
+    object.
+
+    Args:
+        request: Reference to the current request object.
+
+    Returns:
+       An :class:`oauth2.client.Storage` object.
+    """
+    storage_model = oauth2_settings.storage_model
+    user_property = oauth2_settings.storage_model_user_property
+    credentials_property = oauth2_settings.storage_model_credentials_property
+
+    if storage_model:
+        module_name, class_name = storage_model.rsplit('.', 1)
+        module = importlib.import_module(module_name)
+        storage_model_class = getattr(module, class_name)
+        return storage.DjangoORMStorage(storage_model_class,
+                                        user_property,
+                                        request.user,
+                                        credentials_property)
+    else:
+        # use session
+        return dictionary_storage.DictionaryStorage(
+            request.session, key=_CREDENTIALS_KEY)
+
+
+def _redirect_with_params(url_name, *args, **kwargs):
+    """Helper method to create a redirect response with URL params.
+
+    This builds a redirect string that converts kwargs into a
+    query string.
+
+    Args:
+        url_name: The name of the url to redirect to.
+        kwargs: the query string param and their values to build.
+
+    Returns:
+        A properly formatted redirect string.
+    """
+    url = urlresolvers.reverse(url_name, args=args)
+    params = parse.urlencode(kwargs, True)
+    return "{0}?{1}".format(url, params)
+
+
+def _credentials_from_request(request):
+    """Gets the authorized credentials for this flow, if they exist."""
+    # ORM storage requires a logged in user
+    if (oauth2_settings.storage_model is None or
+            request.user.is_authenticated()):
+        return get_storage(request).get()
+    else:
+        return None
+
+
+class UserOAuth2(object):
+    """Class to create oauth2 objects on Django request objects containing
+    credentials and helper methods.
+    """
+
+    def __init__(self, request, scopes=None, return_url=None):
+        """Initialize the Oauth2 Object.
+
+        Args:
+            request: Django request object.
+            scopes: Scopes desired for this OAuth2 flow.
+            return_url: The url to return to after the OAuth flow is complete,
+                 defaults to the request's current URL path.
+        """
+        self.request = request
+        self.return_url = return_url or request.get_full_path()
+        if scopes:
+            self._scopes = set(oauth2_settings.scopes) | set(scopes)
+        else:
+            self._scopes = set(oauth2_settings.scopes)
+
+    def get_authorize_redirect(self):
+        """Creates a URl to start the OAuth2 authorization flow."""
+        get_params = {
+            'return_url': self.return_url,
+            'scopes': self._get_scopes()
+        }
+
+        return _redirect_with_params('google_oauth:authorize', **get_params)
+
+    def has_credentials(self):
+        """Returns True if there are valid credentials for the current user
+        and required scopes."""
+        credentials = _credentials_from_request(self.request)
+        return (credentials and not credentials.invalid and
+                credentials.has_scopes(self._get_scopes()))
+
+    def _get_scopes(self):
+        """Returns the scopes associated with this object, kept up to
+         date for incremental auth."""
+        if _credentials_from_request(self.request):
+            return (self._scopes |
+                    _credentials_from_request(self.request).scopes)
+        else:
+            return self._scopes
+
+    @property
+    def scopes(self):
+        """Returns the scopes associated with this OAuth2 object."""
+        # make sure previously requested custom scopes are maintained
+        # in future authorizations
+        return self._get_scopes()
+
+    @property
+    def credentials(self):
+        """Gets the authorized credentials for this flow, if they exist."""
+        return _credentials_from_request(self.request)
+
+    @property
+    def http(self):
+        """Helper method to create an HTTP client authorized with OAuth2
+        credentials."""
+        if self.has_credentials():
+            return self.credentials.authorize(httplib2.Http())
+        return None
diff --git a/oauth2client/contrib/django_util/apps.py b/oauth2client/contrib/django_util/apps.py
new file mode 100644
index 0000000..86676b9
--- /dev/null
+++ b/oauth2client/contrib/django_util/apps.py
@@ -0,0 +1,32 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Application Config For Django OAuth2 Helper.
+
+Django 1.7+ provides an
+[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
+API so that Django projects can introspect on installed applications using a
+stable API. This module exists to follow that convention.
+"""
+
+import sys
+
+# Django 1.7+ only supports Python 2.7+
+if sys.hexversion >= 0x02070000:  # pragma: NO COVER
+    from django.apps import AppConfig
+
+    class GoogleOAuth2HelperConfig(AppConfig):
+        """ App Config for Django Helper"""
+        name = 'oauth2client.django_util'
+        verbose_name = "Google OAuth2 Django Helper"
diff --git a/oauth2client/contrib/django_util/decorators.py b/oauth2client/contrib/django_util/decorators.py
new file mode 100644
index 0000000..e62e171
--- /dev/null
+++ b/oauth2client/contrib/django_util/decorators.py
@@ -0,0 +1,145 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Decorators for Django OAuth2 Flow.
+
+Contains two decorators, ``oauth_required`` and ``oauth_enabled``.
+
+``oauth_required`` will ensure that a user has an oauth object containing
+credentials associated with the request, and if not, redirect to the
+authorization flow.
+
+``oauth_enabled`` will attach the oauth2 object containing credentials if it
+exists. If it doesn't, the view will still render, but helper methods will be
+attached to start the oauth2 flow.
+"""
+
+from django import shortcuts
+import django.conf
+from six import wraps
+from six.moves.urllib import parse
+
+from oauth2client.contrib import django_util
+
+
+def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
+    """ Decorator to require OAuth2 credentials for a view.
+
+
+    .. code-block:: python
+       :caption: views.py
+       :name: views_required_2
+
+
+       from oauth2client.django_util.decorators import oauth_required
+
+       @oauth_required
+       def requires_default_scopes(request):
+          email = request.credentials.id_token['email']
+          service = build(serviceName='calendar', version='v3',
+                       http=request.oauth.http,
+                       developerKey=API_KEY)
+          events = service.events().list(
+                                    calendarId='primary').execute()['items']
+          return HttpResponse(
+              "email: {0}, calendar: {1}".format(email, str(events)))
+
+    Args:
+        decorated_function: View function to decorate, must have the Django
+           request object as the first argument.
+        scopes: Scopes to require, will default.
+        decorator_kwargs: Can include ``return_url`` to specify the URL to
+           return to after OAuth2 authorization is complete.
+
+    Returns:
+        An OAuth2 Authorize view if credentials are not found or if the
+        credentials are missing the required scopes. Otherwise,
+        the decorated view.
+    """
+    def curry_wrapper(wrapped_function):
+        @wraps(wrapped_function)
+        def required_wrapper(request, *args, **kwargs):
+            if not (django_util.oauth2_settings.storage_model is None or
+                    request.user.is_authenticated()):
+                redirect_str = '{0}?next={1}'.format(
+                    django.conf.settings.LOGIN_URL,
+                    parse.quote(request.path))
+                return shortcuts.redirect(redirect_str)
+
+            return_url = decorator_kwargs.pop('return_url',
+                                              request.get_full_path())
+            user_oauth = django_util.UserOAuth2(request, scopes, return_url)
+            if not user_oauth.has_credentials():
+                return shortcuts.redirect(user_oauth.get_authorize_redirect())
+            setattr(request, django_util.oauth2_settings.request_prefix,
+                    user_oauth)
+            return wrapped_function(request, *args, **kwargs)
+
+        return required_wrapper
+
+    if decorated_function:
+        return curry_wrapper(decorated_function)
+    else:
+        return curry_wrapper
+
+
+def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
+    """ Decorator to enable OAuth Credentials if authorized, and setup
+    the oauth object on the request object to provide helper functions
+    to start the flow otherwise.
+
+    .. code-block:: python
+       :caption: views.py
+       :name: views_enabled3
+
+       from oauth2client.django_util.decorators import oauth_enabled
+
+       @oauth_enabled
+       def optional_oauth2(request):
+           if request.oauth.has_credentials():
+               # this could be passed into a view
+               # request.oauth.http is also initialized
+               return HttpResponse("User email: {0}".format(
+                                   request.oauth.credentials.id_token['email'])
+           else:
+               return HttpResponse('Here is an OAuth Authorize link:
+               <a href="{0}">Authorize</a>'.format(
+                   request.oauth.get_authorize_redirect()))
+
+
+    Args:
+        decorated_function: View function to decorate.
+        scopes: Scopes to require, will default.
+        decorator_kwargs: Can include ``return_url`` to specify the URL to
+           return to after OAuth2 authorization is complete.
+
+    Returns:
+         The decorated view function.
+    """
+    def curry_wrapper(wrapped_function):
+        @wraps(wrapped_function)
+        def enabled_wrapper(request, *args, **kwargs):
+            return_url = decorator_kwargs.pop('return_url',
+                                              request.get_full_path())
+            user_oauth = django_util.UserOAuth2(request, scopes, return_url)
+            setattr(request, django_util.oauth2_settings.request_prefix,
+                    user_oauth)
+            return wrapped_function(request, *args, **kwargs)
+
+        return enabled_wrapper
+
+    if decorated_function:
+        return curry_wrapper(decorated_function)
+    else:
+        return curry_wrapper
diff --git a/oauth2client/contrib/django_util/models.py b/oauth2client/contrib/django_util/models.py
new file mode 100644
index 0000000..87e1da7
--- /dev/null
+++ b/oauth2client/contrib/django_util/models.py
@@ -0,0 +1,75 @@
+# Copyright 2016 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Contains classes used for the Django ORM storage."""
+
+import base64
+import pickle
+
+from django.db import models
+from django.utils import encoding
+
+import oauth2client
+
+
+class CredentialsField(models.Field):
+    """Django ORM field for storing OAuth2 Credentials."""
+
+    def __init__(self, *args, **kwargs):
+        if 'null' not in kwargs:
+            kwargs['null'] = True
+        super(CredentialsField, self).__init__(*args, **kwargs)
+
+    def get_internal_type(self):
+        return 'BinaryField'
+
+    def from_db_value(self, value, expression, connection, context):
+        """Overrides ``models.Field`` method. This converts the value
+        returned from the database to an instance of this class.
+        """
+        return self.to_python(value)
+
+    def to_python(self, value):
+        """Overrides ``models.Field`` method. This is used to convert
+        bytes (from serialization etc) to an instance of this class"""
+        if value is None:
+            return None
+        elif isinstance(value, oauth2client.client.Credentials):
+            return value
+        else:
+            return pickle.loads(base64.b64decode(encoding.smart_bytes(value)))
+
+    def get_prep_value(self, value):
+        """Overrides ``models.Field`` method. This is used to convert
+        the value from an instances of this class to bytes that can be
+        inserted into the database.
+        """
+        if value is None:
+            return None
+        else:
+            return encoding.smart_text(base64.b64encode(pickle.dumps(value)))
+
+    def value_to_string(self, obj):
+        """Convert the field value from the provided model to a string.
+
+        Used during model serialization.
+
+        Args:
+            obj: db.Model, model object
+
+        Returns:
+            string, the serialized field value
+        """
+        value = self._get_val_from_obj(obj)
+        return self.get_prep_value(value)
diff --git a/oauth2client/contrib/django_util/signals.py b/oauth2client/contrib/django_util/signals.py
new file mode 100644
index 0000000..e9356b4
--- /dev/null
+++ b/oauth2client/contrib/django_util/signals.py
@@ -0,0 +1,28 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Signals for Google OAuth2 Helper.
+
+This module contains signals for Google OAuth2 Helper. Currently it only
+contains one, which fires when an OAuth2 authorization flow has completed.
+"""
+
+import django.dispatch
+
+"""Signal that fires when  OAuth2 Flow has completed.
+It passes the Django request object and the OAuth2 credentials object to the
+ receiver.
+"""
+oauth2_authorized = django.dispatch.Signal(
+    providing_args=["request", "credentials"])
diff --git a/oauth2client/contrib/django_util/site.py b/oauth2client/contrib/django_util/site.py
new file mode 100644
index 0000000..631f79b
--- /dev/null
+++ b/oauth2client/contrib/django_util/site.py
@@ -0,0 +1,26 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Contains Django URL patterns used for OAuth2 flow."""
+
+from django.conf import urls
+
+from oauth2client.contrib.django_util import views
+
+urlpatterns = [
+    urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
+    urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
+]
+
+urls = (urlpatterns, "google_oauth", "google_oauth")
diff --git a/oauth2client/contrib/django_util/storage.py b/oauth2client/contrib/django_util/storage.py
new file mode 100644
index 0000000..5682919
--- /dev/null
+++ b/oauth2client/contrib/django_util/storage.py
@@ -0,0 +1,81 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Contains a storage module that stores credentials using the Django ORM."""
+
+from oauth2client import client
+
+
+class DjangoORMStorage(client.Storage):
+    """Store and retrieve a single credential to and from the Django datastore.
+
+    This Storage helper presumes the Credentials
+    have been stored as a CredentialsField
+    on a db model class.
+    """
+
+    def __init__(self, model_class, key_name, key_value, property_name):
+        """Constructor for Storage.
+
+        Args:
+            model: string, fully qualified name of db.Model model class.
+            key_name: string, key name for the entity that has the credentials
+            key_value: string, key value for the entity that has the
+               credentials.
+            property_name: string, name of the property that is an
+                           CredentialsProperty.
+        """
+        super(DjangoORMStorage, self).__init__()
+        self.model_class = model_class
+        self.key_name = key_name
+        self.key_value = key_value
+        self.property_name = property_name
+
+    def locked_get(self):
+        """Retrieve stored credential from the Django ORM.
+
+        Returns:
+            oauth2client.Credentials retrieved from the Django ORM, associated
+             with the ``model``, ``key_value``->``key_name`` pair used to query
+             for the model, and ``property_name`` identifying the
+             ``CredentialsProperty`` field, all of which are defined in the
+             constructor for this Storage object.
+
+        """
+        query = {self.key_name: self.key_value}
+        entities = self.model_class.objects.filter(**query)
+        if len(entities) > 0:
+            credential = getattr(entities[0], self.property_name)
+            if getattr(credential, 'set_store', None) is not None:
+                credential.set_store(self)
+            return credential
+        else:
+            return None
+
+    def locked_put(self, credentials):
+        """Write a Credentials to the Django datastore.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        entity, _ = self.model_class.objects.get_or_create(
+            **{self.key_name: self.key_value})
+
+        setattr(entity, self.property_name, credentials)
+        entity.save()
+
+    def locked_delete(self):
+        """Delete Credentials from the datastore."""
+        query = {self.key_name: self.key_value}
+        self.model_class.objects.filter(**query).delete()
diff --git a/oauth2client/contrib/django_util/views.py b/oauth2client/contrib/django_util/views.py
new file mode 100644
index 0000000..4d8ae03
--- /dev/null
+++ b/oauth2client/contrib/django_util/views.py
@@ -0,0 +1,190 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""This module contains the views used by the OAuth2 flows.
+
+Their are two views used by the OAuth2 flow, the authorize and the callback
+view. The authorize view kicks off the three-legged OAuth flow, and the
+callback view validates the flow and if successful stores the credentials
+in the configured storage."""
+
+import hashlib
+import json
+import os
+import pickle
+
+from django import http
+from django import shortcuts
+from django.conf import settings
+from django.core import urlresolvers
+from django.shortcuts import redirect
+from six.moves.urllib import parse
+
+from oauth2client import client
+from oauth2client.contrib import django_util
+from oauth2client.contrib.django_util import get_storage
+from oauth2client.contrib.django_util import signals
+
+_CSRF_KEY = 'google_oauth2_csrf_token'
+_FLOW_KEY = 'google_oauth2_flow_{0}'
+
+
+def _make_flow(request, scopes, return_url=None):
+    """Creates a Web Server Flow
+
+    Args:
+        request: A Django request object.
+        scopes: the request oauth2 scopes.
+        return_url: The URL to return to after the flow is complete. Defaults
+            to the path of the current request.
+
+    Returns:
+        An OAuth2 flow object that has been stored in the session.
+    """
+    # Generate a CSRF token to prevent malicious requests.
+    csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
+
+    request.session[_CSRF_KEY] = csrf_token
+
+    state = json.dumps({
+        'csrf_token': csrf_token,
+        'return_url': return_url,
+    })
+
+    flow = client.OAuth2WebServerFlow(
+        client_id=django_util.oauth2_settings.client_id,
+        client_secret=django_util.oauth2_settings.client_secret,
+        scope=scopes,
+        state=state,
+        redirect_uri=request.build_absolute_uri(
+            urlresolvers.reverse("google_oauth:callback")))
+
+    flow_key = _FLOW_KEY.format(csrf_token)
+    request.session[flow_key] = pickle.dumps(flow)
+    return flow
+
+
+def _get_flow_for_token(csrf_token, request):
+    """ Looks up the flow in session to recover information about requested
+    scopes.
+
+    Args:
+        csrf_token: The token passed in the callback request that should
+            match the one previously generated and stored in the request on the
+            initial authorization view.
+
+    Returns:
+        The OAuth2 Flow object associated with this flow based on the
+        CSRF token.
+    """
+    flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
+    return None if flow_pickle is None else pickle.loads(flow_pickle)
+
+
+def oauth2_callback(request):
+    """ View that handles the user's return from OAuth2 provider.
+
+    This view verifies the CSRF state and OAuth authorization code, and on
+    success stores the credentials obtained in the storage provider,
+    and redirects to the return_url specified in the authorize view and
+    stored in the session.
+
+    Args:
+        request: Django request.
+
+    Returns:
+         A redirect response back to the return_url.
+    """
+    if 'error' in request.GET:
+        reason = request.GET.get(
+            'error_description', request.GET.get('error', ''))
+        return http.HttpResponseBadRequest(
+            'Authorization failed {0}'.format(reason))
+
+    try:
+        encoded_state = request.GET['state']
+        code = request.GET['code']
+    except KeyError:
+        return http.HttpResponseBadRequest(
+            'Request missing state or authorization code')
+
+    try:
+        server_csrf = request.session[_CSRF_KEY]
+    except KeyError:
+        return http.HttpResponseBadRequest(
+            'No existing session for this flow.')
+
+    try:
+        state = json.loads(encoded_state)
+        client_csrf = state['csrf_token']
+        return_url = state['return_url']
+    except (ValueError, KeyError):
+        return http.HttpResponseBadRequest('Invalid state parameter.')
+
+    if client_csrf != server_csrf:
+        return http.HttpResponseBadRequest('Invalid CSRF token.')
+
+    flow = _get_flow_for_token(client_csrf, request)
+
+    if not flow:
+        return http.HttpResponseBadRequest('Missing Oauth2 flow.')
+
+    try:
+        credentials = flow.step2_exchange(code)
+    except client.FlowExchangeError as exchange_error:
+        return http.HttpResponseBadRequest(
+            'An error has occurred: {0}'.format(exchange_error))
+
+    get_storage(request).put(credentials)
+
+    signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
+                                   request=request, credentials=credentials)
+
+    return shortcuts.redirect(return_url)
+
+
+def oauth2_authorize(request):
+    """ View to start the OAuth2 Authorization flow.
+
+     This view starts the OAuth2 authorization flow. If scopes is passed in
+     as a  GET URL parameter, it will authorize those scopes, otherwise the
+     default scopes specified in settings. The return_url can also be
+     specified as a GET parameter, otherwise the referer header will be
+     checked, and if that isn't found it will return to the root path.
+
+    Args:
+       request: The Django request object.
+
+    Returns:
+         A redirect to Google OAuth2 Authorization.
+    """
+    return_url = request.GET.get('return_url', None)
+
+    # Model storage (but not session storage) requires a logged in user
+    if django_util.oauth2_settings.storage_model:
+        if not request.user.is_authenticated():
+            return redirect('{0}?next={1}'.format(
+                settings.LOGIN_URL, parse.quote(request.get_full_path())))
+        # This checks for the case where we ended up here because of a logged
+        # out user but we had credentials for it in the first place
+        elif get_storage(request).get() is not None:
+            return redirect(return_url)
+
+    scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
+
+    if not return_url:
+        return_url = request.META.get('HTTP_REFERER', '/')
+    flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
+    auth_url = flow.step1_get_authorize_url()
+    return shortcuts.redirect(auth_url)
diff --git a/oauth2client/contrib/flask_util.py b/oauth2client/contrib/flask_util.py
new file mode 100644
index 0000000..47c3df1
--- /dev/null
+++ b/oauth2client/contrib/flask_util.py
@@ -0,0 +1,556 @@
+# Copyright 2015 Google Inc.  All rights reserved.
+#
+# 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.
+
+"""Utilities for the Flask web framework
+
+Provides a Flask extension that makes using OAuth2 web server flow easier.
+The extension includes views that handle the entire auth flow and a
+``@required`` decorator to automatically ensure that user credentials are
+available.
+
+
+Configuration
+=============
+
+To configure, you'll need a set of OAuth2 web application credentials from the
+`Google Developer's Console <https://console.developers.google.com/project/_/\
+apiui/credential>`__.
+
+.. code-block:: python
+
+    from oauth2client.contrib.flask_util import UserOAuth2
+
+    app = Flask(__name__)
+
+    app.config['SECRET_KEY'] = 'your-secret-key'
+
+    app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json'
+
+    # or, specify the client id and secret separately
+    app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
+    app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
+
+    oauth2 = UserOAuth2(app)
+
+
+Usage
+=====
+
+Once configured, you can use the :meth:`UserOAuth2.required` decorator to
+ensure that credentials are available within a view.
+
+.. code-block:: python
+   :emphasize-lines: 3,7,10
+
+    # Note that app.route should be the outermost decorator.
+    @app.route('/needs_credentials')
+    @oauth2.required
+    def example():
+        # http is authorized with the user's credentials and can be used
+        # to make http calls.
+        http = oauth2.http()
+
+        # Or, you can access the credentials directly
+        credentials = oauth2.credentials
+
+If you want credentials to be optional for a view, you can leave the decorator
+off and use :meth:`UserOAuth2.has_credentials` to check.
+
+.. code-block:: python
+   :emphasize-lines: 3
+
+    @app.route('/optional')
+    def optional():
+        if oauth2.has_credentials():
+            return 'Credentials found!'
+        else:
+            return 'No credentials!'
+
+
+When credentials are available, you can use :attr:`UserOAuth2.email` and
+:attr:`UserOAuth2.user_id` to access information from the `ID Token
+<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, if
+available.
+
+.. code-block:: python
+   :emphasize-lines: 4
+
+    @app.route('/info')
+    @oauth2.required
+    def info():
+        return "Hello, {} ({})".format(oauth2.email, oauth2.user_id)
+
+
+URLs & Trigging Authorization
+=============================
+
+The extension will add two new routes to your application:
+
+    * ``"oauth2.authorize"`` -> ``/oauth2authorize``
+    * ``"oauth2.callback"`` -> ``/oauth2callback``
+
+When configuring your OAuth2 credentials on the Google Developer's Console, be
+sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized
+callback url.
+
+Typically you don't not need to use these routes directly, just be sure to
+decorate any views that require credentials with ``@oauth2.required``. If
+needed, you can trigger authorization at any time by redirecting the user
+to the URL returned by :meth:`UserOAuth2.authorize_url`.
+
+.. code-block:: python
+   :emphasize-lines: 3
+
+    @app.route('/login')
+    def login():
+        return oauth2.authorize_url("/")
+
+
+Incremental Auth
+================
+
+This extension also supports `Incremental Auth <https://developers.google.com\
+/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it,
+configure the extension with ``include_granted_scopes``.
+
+.. code-block:: python
+
+    oauth2 = UserOAuth2(app, include_granted_scopes=True)
+
+Then specify any additional scopes needed on the decorator, for example:
+
+.. code-block:: python
+   :emphasize-lines: 2,7
+
+    @app.route('/drive')
+    @oauth2.required(scopes=["https://www.googleapis.com/auth/drive"])
+    def requires_drive():
+        ...
+
+    @app.route('/calendar')
+    @oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"])
+    def requires_calendar():
+        ...
+
+The decorator will ensure that the the user has authorized all specified scopes
+before allowing them to access the view, and will also ensure that credentials
+do not lose any previously authorized scopes.
+
+
+Storage
+=======
+
+By default, the extension uses a Flask session-based storage solution. This
+means that credentials are only available for the duration of a session. It
+also means that with Flask's default configuration, the credentials will be
+visible in the session cookie. It's highly recommended to use database-backed
+session and to use https whenever handling user credentials.
+
+If you need the credentials to be available longer than a user session or
+available outside of a request context, you will need to implement your own
+:class:`oauth2client.Storage`.
+"""
+
+from functools import wraps
+import hashlib
+import json
+import os
+import pickle
+
+try:
+    from flask import Blueprint
+    from flask import _app_ctx_stack
+    from flask import current_app
+    from flask import redirect
+    from flask import request
+    from flask import session
+    from flask import url_for
+except ImportError:  # pragma: NO COVER
+    raise ImportError('The flask utilities require flask 0.9 or newer.')
+
+import httplib2
+import six.moves.http_client as httplib
+
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client.contrib import dictionary_storage
+
+
+__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
+
+_DEFAULT_SCOPES = ('email',)
+_CREDENTIALS_KEY = 'google_oauth2_credentials'
+_FLOW_KEY = 'google_oauth2_flow_{0}'
+_CSRF_KEY = 'google_oauth2_csrf_token'
+
+
+def _get_flow_for_token(csrf_token):
+    """Retrieves the flow instance associated with a given CSRF token from
+    the Flask session."""
+    flow_pickle = session.pop(
+        _FLOW_KEY.format(csrf_token), None)
+
+    if flow_pickle is None:
+        return None
+    else:
+        return pickle.loads(flow_pickle)
+
+
+class UserOAuth2(object):
+    """Flask extension for making OAuth 2.0 easier.
+
+    Configuration values:
+
+        * ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json
+          file, obtained from the credentials screen in the Google Developers
+          console.
+        * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
+          is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not
+          specified.
+        * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
+          secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE``
+          is not specified.
+
+    If app is specified, all arguments will be passed along to init_app.
+
+    If no app is specified, then you should call init_app in your application
+    factory to finish initialization.
+    """
+
+    def __init__(self, app=None, *args, **kwargs):
+        self.app = app
+        if app is not None:
+            self.init_app(app, *args, **kwargs)
+
+    def init_app(self, app, scopes=None, client_secrets_file=None,
+                 client_id=None, client_secret=None, authorize_callback=None,
+                 storage=None, **kwargs):
+        """Initialize this extension for the given app.
+
+        Arguments:
+            app: A Flask application.
+            scopes: Optional list of scopes to authorize.
+            client_secrets_file: Path to a file containing client secrets. You
+                can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config
+                value.
+            client_id: If not specifying a client secrets file, specify the
+                OAuth2 client id. You can also specify the
+                GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
+                client secret.
+            client_secret: The OAuth2 client secret. You can also specify the
+                GOOGLE_OAUTH2_CLIENT_SECRET config value.
+            authorize_callback: A function that is executed after successful
+                user authorization.
+            storage: A oauth2client.client.Storage subclass for storing the
+                credentials. By default, this is a Flask session based storage.
+            kwargs: Any additional args are passed along to the Flow
+                constructor.
+        """
+        self.app = app
+        self.authorize_callback = authorize_callback
+        self.flow_kwargs = kwargs
+
+        if storage is None:
+            storage = dictionary_storage.DictionaryStorage(
+                session, key=_CREDENTIALS_KEY)
+        self.storage = storage
+
+        if scopes is None:
+            scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
+        self.scopes = scopes
+
+        self._load_config(client_secrets_file, client_id, client_secret)
+
+        app.register_blueprint(self._create_blueprint())
+
+    def _load_config(self, client_secrets_file, client_id, client_secret):
+        """Loads oauth2 configuration in order of priority.
+
+        Priority:
+            1. Config passed to the constructor or init_app.
+            2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
+               config.
+            3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
+               GOOGLE_OAUTH2_CLIENT_SECRET app config.
+
+        Raises:
+            ValueError if no config could be found.
+        """
+        if client_id and client_secret:
+            self.client_id, self.client_secret = client_id, client_secret
+            return
+
+        if client_secrets_file:
+            self._load_client_secrets(client_secrets_file)
+            return
+
+        if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
+            self._load_client_secrets(
+                self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
+            return
+
+        try:
+            self.client_id, self.client_secret = (
+                self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
+                self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
+        except KeyError:
+            raise ValueError(
+                'OAuth2 configuration could not be found. Either specify the '
+                'client_secrets_file or client_id and client_secret or set '
+                'the app configuration variables '
+                'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
+                'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
+
+    def _load_client_secrets(self, filename):
+        """Loads client secrets from the given filename."""
+        client_type, client_info = clientsecrets.loadfile(filename)
+        if client_type != clientsecrets.TYPE_WEB:
+            raise ValueError(
+                'The flow specified in {0} is not supported.'.format(
+                    client_type))
+
+        self.client_id = client_info['client_id']
+        self.client_secret = client_info['client_secret']
+
+    def _make_flow(self, return_url=None, **kwargs):
+        """Creates a Web Server Flow"""
+        # Generate a CSRF token to prevent malicious requests.
+        csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
+
+        session[_CSRF_KEY] = csrf_token
+
+        state = json.dumps({
+            'csrf_token': csrf_token,
+            'return_url': return_url
+        })
+
+        kw = self.flow_kwargs.copy()
+        kw.update(kwargs)
+
+        extra_scopes = kw.pop('scopes', [])
+        scopes = set(self.scopes).union(set(extra_scopes))
+
+        flow = client.OAuth2WebServerFlow(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            scope=scopes,
+            state=state,
+            redirect_uri=url_for('oauth2.callback', _external=True),
+            **kw)
+
+        flow_key = _FLOW_KEY.format(csrf_token)
+        session[flow_key] = pickle.dumps(flow)
+
+        return flow
+
+    def _create_blueprint(self):
+        bp = Blueprint('oauth2', __name__)
+        bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
+        bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
+
+        return bp
+
+    def authorize_view(self):
+        """Flask view that starts the authorization flow.
+
+        Starts flow by redirecting the user to the OAuth2 provider.
+        """
+        args = request.args.to_dict()
+
+        # Scopes will be passed as mutliple args, and to_dict() will only
+        # return one. So, we use getlist() to get all of the scopes.
+        args['scopes'] = request.args.getlist('scopes')
+
+        return_url = args.pop('return_url', None)
+        if return_url is None:
+            return_url = request.referrer or '/'
+
+        flow = self._make_flow(return_url=return_url, **args)
+        auth_url = flow.step1_get_authorize_url()
+
+        return redirect(auth_url)
+
+    def callback_view(self):
+        """Flask view that handles the user's return from OAuth2 provider.
+
+        On return, exchanges the authorization code for credentials and stores
+        the credentials.
+        """
+        if 'error' in request.args:
+            reason = request.args.get(
+                'error_description', request.args.get('error', ''))
+            return ('Authorization failed: {0}'.format(reason),
+                    httplib.BAD_REQUEST)
+
+        try:
+            encoded_state = request.args['state']
+            server_csrf = session[_CSRF_KEY]
+            code = request.args['code']
+        except KeyError:
+            return 'Invalid request', httplib.BAD_REQUEST
+
+        try:
+            state = json.loads(encoded_state)
+            client_csrf = state['csrf_token']
+            return_url = state['return_url']
+        except (ValueError, KeyError):
+            return 'Invalid request state', httplib.BAD_REQUEST
+
+        if client_csrf != server_csrf:
+            return 'Invalid request state', httplib.BAD_REQUEST
+
+        flow = _get_flow_for_token(server_csrf)
+
+        if flow is None:
+            return 'Invalid request state', httplib.BAD_REQUEST
+
+        # Exchange the auth code for credentials.
+        try:
+            credentials = flow.step2_exchange(code)
+        except client.FlowExchangeError as exchange_error:
+            current_app.logger.exception(exchange_error)
+            content = 'An error occurred: {0}'.format(exchange_error)
+            return content, httplib.BAD_REQUEST
+
+        # Save the credentials to the storage.
+        self.storage.put(credentials)
+
+        if self.authorize_callback:
+            self.authorize_callback(credentials)
+
+        return redirect(return_url)
+
+    @property
+    def credentials(self):
+        """The credentials for the current user or None if unavailable."""
+        ctx = _app_ctx_stack.top
+
+        if not hasattr(ctx, _CREDENTIALS_KEY):
+            ctx.google_oauth2_credentials = self.storage.get()
+
+        return ctx.google_oauth2_credentials
+
+    def has_credentials(self):
+        """Returns True if there are valid credentials for the current user."""
+        if not self.credentials:
+            return False
+        # Is the access token expired? If so, do we have an refresh token?
+        elif (self.credentials.access_token_expired and
+                not self.credentials.refresh_token):
+            return False
+        else:
+            return True
+
+    @property
+    def email(self):
+        """Returns the user's email address or None if there are no credentials.
+
+        The email address is provided by the current credentials' id_token.
+        This should not be used as unique identifier as the user can change
+        their email. If you need a unique identifier, use user_id.
+        """
+        if not self.credentials:
+            return None
+        try:
+            return self.credentials.id_token['email']
+        except KeyError:
+            current_app.logger.error(
+                'Invalid id_token {0}'.format(self.credentials.id_token))
+
+    @property
+    def user_id(self):
+        """Returns the a unique identifier for the user
+
+        Returns None if there are no credentials.
+
+        The id is provided by the current credentials' id_token.
+        """
+        if not self.credentials:
+            return None
+        try:
+            return self.credentials.id_token['sub']
+        except KeyError:
+            current_app.logger.error(
+                'Invalid id_token {0}'.format(self.credentials.id_token))
+
+    def authorize_url(self, return_url, **kwargs):
+        """Creates a URL that can be used to start the authorization flow.
+
+        When the user is directed to the URL, the authorization flow will
+        begin. Once complete, the user will be redirected to the specified
+        return URL.
+
+        Any kwargs are passed into the flow constructor.
+        """
+        return url_for('oauth2.authorize', return_url=return_url, **kwargs)
+
+    def required(self, decorated_function=None, scopes=None,
+                 **decorator_kwargs):
+        """Decorator to require OAuth2 credentials for a view.
+
+        If credentials are not available for the current user, then they will
+        be redirected to the authorization flow. Once complete, the user will
+        be redirected back to the original page.
+        """
+
+        def curry_wrapper(wrapped_function):
+            @wraps(wrapped_function)
+            def required_wrapper(*args, **kwargs):
+                return_url = decorator_kwargs.pop('return_url', request.url)
+
+                requested_scopes = set(self.scopes)
+                if scopes is not None:
+                    requested_scopes |= set(scopes)
+                if self.has_credentials():
+                    requested_scopes |= self.credentials.scopes
+
+                requested_scopes = list(requested_scopes)
+
+                # Does the user have credentials and does the credentials have
+                # all of the needed scopes?
+                if (self.has_credentials() and
+                        self.credentials.has_scopes(requested_scopes)):
+                    return wrapped_function(*args, **kwargs)
+                # Otherwise, redirect to authorization
+                else:
+                    auth_url = self.authorize_url(
+                        return_url,
+                        scopes=requested_scopes,
+                        **decorator_kwargs)
+
+                    return redirect(auth_url)
+
+            return required_wrapper
+
+        if decorated_function:
+            return curry_wrapper(decorated_function)
+        else:
+            return curry_wrapper
+
+    def http(self, *args, **kwargs):
+        """Returns an authorized http instance.
+
+        Can only be called if there are valid credentials for the user, such
+        as inside of a view that is decorated with @required.
+
+        Args:
+            *args: Positional arguments passed to httplib2.Http constructor.
+            **kwargs: Positional arguments passed to httplib2.Http constructor.
+
+        Raises:
+            ValueError if no credentials are available.
+        """
+        if not self.credentials:
+            raise ValueError('No credentials available.')
+        return self.credentials.authorize(httplib2.Http(*args, **kwargs))
diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py
new file mode 100644
index 0000000..f3a6ca1
--- /dev/null
+++ b/oauth2client/contrib/gce.py
@@ -0,0 +1,162 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Utilities for Google Compute Engine
+
+Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
+"""
+
+import logging
+import warnings
+
+import httplib2
+
+from oauth2client import client
+from oauth2client.contrib import _metadata
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+logger = logging.getLogger(__name__)
+
+_SCOPES_WARNING = """\
+You have requested explicit scopes to be used with a GCE service account.
+Using this argument will have no effect on the actual scopes for tokens
+requested. These scopes are set at VM instance creation time and
+can't be overridden in the request.
+"""
+
+
+class AppAssertionCredentials(client.AssertionCredentials):
+    """Credentials object for Compute Engine Assertion Grants
+
+    This object will allow a Compute Engine instance to identify itself to
+    Google and other OAuth 2.0 servers that can verify assertions. It can be
+    used for the purpose of accessing data stored under an account assigned to
+    the Compute Engine instance itself.
+
+    This credential does not require a flow to instantiate because it
+    represents a two legged flow, and therefore has all of the required
+    information to generate and refresh its own access tokens.
+
+    Note that :attr:`service_account_email` and :attr:`scopes`
+    will both return None until the credentials have been refreshed.
+    To check whether credentials have previously been refreshed use
+    :attr:`invalid`.
+    """
+
+    def __init__(self, email=None, *args, **kwargs):
+        """Constructor for AppAssertionCredentials
+
+        Args:
+            email: an email that specifies the service account to use.
+                   Only necessary if using custom service accounts
+                   (see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount).
+        """
+        if 'scopes' in kwargs:
+            warnings.warn(_SCOPES_WARNING)
+            kwargs['scopes'] = None
+
+        # Assertion type is no longer used, but still in the
+        # parent class signature.
+        super(AppAssertionCredentials, self).__init__(None, *args, **kwargs)
+
+        self.service_account_email = email
+        self.scopes = None
+        self.invalid = True
+
+    @classmethod
+    def from_json(cls, json_data):
+        raise NotImplementedError(
+            'Cannot serialize credentials for GCE service accounts.')
+
+    def to_json(self):
+        raise NotImplementedError(
+            'Cannot serialize credentials for GCE service accounts.')
+
+    def retrieve_scopes(self, http):
+        """Retrieves the canonical list of scopes for this access token.
+
+        Overrides client.Credentials.retrieve_scopes. Fetches scopes info
+        from the metadata server.
+
+        Args:
+            http: httplib2.Http, an http object to be used to make the refresh
+                  request.
+
+        Returns:
+            A set of strings containing the canonical list of scopes.
+        """
+        self._retrieve_info(http.request)
+        return self.scopes
+
+    def _retrieve_info(self, http_request):
+        """Validates invalid service accounts by retrieving service account info.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make the
+                          request to the metadata server
+        """
+        if self.invalid:
+            info = _metadata.get_service_account_info(
+                http_request,
+                service_account=self.service_account_email or 'default')
+            self.invalid = False
+            self.service_account_email = info['email']
+            self.scopes = info['scopes']
+
+    def _refresh(self, http_request):
+        """Refreshes the access_token.
+
+        Skip all the storage hoops and just refresh using the API.
+
+        Args:
+            http_request: callable, a callable that matches the method
+                          signature of httplib2.Http.request, used to make
+                          the refresh request.
+
+        Raises:
+            HttpAccessTokenRefreshError: When the refresh fails.
+        """
+        try:
+            self._retrieve_info(http_request)
+            self.access_token, self.token_expiry = _metadata.get_token(
+                http_request, service_account=self.service_account_email)
+        except httplib2.HttpLib2Error as e:
+            raise client.HttpAccessTokenRefreshError(str(e))
+
+    @property
+    def serialization_data(self):
+        raise NotImplementedError(
+            'Cannot serialize credentials for GCE service accounts.')
+
+    def create_scoped_required(self):
+        return False
+
+    def sign_blob(self, blob):
+        """Cryptographically sign a blob (of bytes).
+
+        This method is provided to support a common interface, but
+        the actual key used for a Google Compute Engine service account
+        is not available, so it can't be used to sign content.
+
+        Args:
+            blob: bytes, Message to be signed.
+
+        Raises:
+            NotImplementedError, always.
+        """
+        raise NotImplementedError(
+            'Compute Engine service accounts cannot sign blobs')
diff --git a/oauth2client/contrib/keyring_storage.py b/oauth2client/contrib/keyring_storage.py
new file mode 100644
index 0000000..f4f2e30
--- /dev/null
+++ b/oauth2client/contrib/keyring_storage.py
@@ -0,0 +1,98 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""A keyring based Storage.
+
+A Storage for Credentials that uses the keyring module.
+"""
+
+import threading
+
+import keyring
+
+from oauth2client import client
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class Storage(client.Storage):
+    """Store and retrieve a single credential to and from the keyring.
+
+    To use this module you must have the keyring module installed. See
+    <http://pypi.python.org/pypi/keyring/>. This is an optional module and is
+    not installed with oauth2client by default because it does not work on all
+    the platforms that oauth2client supports, such as Google App Engine.
+
+    The keyring module <http://pypi.python.org/pypi/keyring/> is a
+    cross-platform library for access the keyring capabilities of the local
+    system. The user will be prompted for their keyring password when this
+    module is used, and the manner in which the user is prompted will vary per
+    platform.
+
+    Usage::
+
+        from oauth2client import keyring_storage
+
+        s = keyring_storage.Storage('name_of_application', 'user1')
+        credentials = s.get()
+
+    """
+
+    def __init__(self, service_name, user_name):
+        """Constructor.
+
+        Args:
+            service_name: string, The name of the service under which the
+                          credentials are stored.
+            user_name: string, The name of the user to store credentials for.
+        """
+        super(Storage, self).__init__(lock=threading.Lock())
+        self._service_name = service_name
+        self._user_name = user_name
+
+    def locked_get(self):
+        """Retrieve Credential from file.
+
+        Returns:
+            oauth2client.client.Credentials
+        """
+        credentials = None
+        content = keyring.get_password(self._service_name, self._user_name)
+
+        if content is not None:
+            try:
+                credentials = client.Credentials.new_from_json(content)
+                credentials.set_store(self)
+            except ValueError:
+                pass
+
+        return credentials
+
+    def locked_put(self, credentials):
+        """Write Credentials to file.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        keyring.set_password(self._service_name, self._user_name,
+                             credentials.to_json())
+
+    def locked_delete(self):
+        """Delete Credentials file.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        keyring.set_password(self._service_name, self._user_name, '')
diff --git a/oauth2client/contrib/locked_file.py b/oauth2client/contrib/locked_file.py
new file mode 100644
index 0000000..0d28ebb
--- /dev/null
+++ b/oauth2client/contrib/locked_file.py
@@ -0,0 +1,234 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Locked file interface that should work on Unix and Windows pythons.
+
+This module first tries to use fcntl locking to ensure serialized access
+to a file, then falls back on a lock file if that is unavialable.
+
+Usage::
+
+    f = LockedFile('filename', 'r+b', 'rb')
+    f.open_and_lock()
+    if f.is_locked():
+      print('Acquired filename with r+b mode')
+      f.file_handle().write('locked data')
+    else:
+      print('Acquired filename with rb mode')
+    f.unlock_and_close()
+
+"""
+
+from __future__ import print_function
+
+import errno
+import logging
+import os
+import time
+
+from oauth2client import util
+
+
+__author__ = 'cache@google.com (David T McWherter)'
+
+logger = logging.getLogger(__name__)
+
+
+class CredentialsFileSymbolicLinkError(Exception):
+    """Credentials files must not be symbolic links."""
+
+
+class AlreadyLockedException(Exception):
+    """Trying to lock a file that has already been locked by the LockedFile."""
+    pass
+
+
+def validate_file(filename):
+    if os.path.islink(filename):
+        raise CredentialsFileSymbolicLinkError(
+            'File: {0} is a symbolic link.'.format(filename))
+
+
+class _Opener(object):
+    """Base class for different locking primitives."""
+
+    def __init__(self, filename, mode, fallback_mode):
+        """Create an Opener.
+
+        Args:
+            filename: string, The pathname of the file.
+            mode: string, The preferred mode to access the file with.
+            fallback_mode: string, The mode to use if locking fails.
+        """
+        self._locked = False
+        self._filename = filename
+        self._mode = mode
+        self._fallback_mode = fallback_mode
+        self._fh = None
+        self._lock_fd = None
+
+    def is_locked(self):
+        """Was the file locked."""
+        return self._locked
+
+    def file_handle(self):
+        """The file handle to the file. Valid only after opened."""
+        return self._fh
+
+    def filename(self):
+        """The filename that is being locked."""
+        return self._filename
+
+    def open_and_lock(self, timeout, delay):
+        """Open the file and lock it.
+
+        Args:
+            timeout: float, How long to try to lock for.
+            delay: float, How long to wait between retries.
+        """
+        pass
+
+    def unlock_and_close(self):
+        """Unlock and close the file."""
+        pass
+
+
+class _PosixOpener(_Opener):
+    """Lock files using Posix advisory lock files."""
+
+    def open_and_lock(self, timeout, delay):
+        """Open the file and lock it.
+
+        Tries to create a .lock file next to the file we're trying to open.
+
+        Args:
+            timeout: float, How long to try to lock for.
+            delay: float, How long to wait between retries.
+
+        Raises:
+            AlreadyLockedException: if the lock is already acquired.
+            IOError: if the open fails.
+            CredentialsFileSymbolicLinkError if the file is a symbolic link.
+        """
+        if self._locked:
+            raise AlreadyLockedException(
+                'File {0} is already locked'.format(self._filename))
+        self._locked = False
+
+        validate_file(self._filename)
+        try:
+            self._fh = open(self._filename, self._mode)
+        except IOError as e:
+            # If we can't access with _mode, try _fallback_mode and don't lock.
+            if e.errno == errno.EACCES:
+                self._fh = open(self._filename, self._fallback_mode)
+                return
+
+        lock_filename = self._posix_lockfile(self._filename)
+        start_time = time.time()
+        while True:
+            try:
+                self._lock_fd = os.open(lock_filename,
+                                        os.O_CREAT | os.O_EXCL | os.O_RDWR)
+                self._locked = True
+                break
+
+            except OSError as e:
+                if e.errno != errno.EEXIST:
+                    raise
+                if (time.time() - start_time) >= timeout:
+                    logger.warn('Could not acquire lock %s in %s seconds',
+                                lock_filename, timeout)
+                    # Close the file and open in fallback_mode.
+                    if self._fh:
+                        self._fh.close()
+                    self._fh = open(self._filename, self._fallback_mode)
+                    return
+                time.sleep(delay)
+
+    def unlock_and_close(self):
+        """Unlock a file by removing the .lock file, and close the handle."""
+        if self._locked:
+            lock_filename = self._posix_lockfile(self._filename)
+            os.close(self._lock_fd)
+            os.unlink(lock_filename)
+            self._locked = False
+            self._lock_fd = None
+        if self._fh:
+            self._fh.close()
+
+    def _posix_lockfile(self, filename):
+        """The name of the lock file to use for posix locking."""
+        return '{0}.lock'.format(filename)
+
+
+class LockedFile(object):
+    """Represent a file that has exclusive access."""
+
+    @util.positional(4)
+    def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
+        """Construct a LockedFile.
+
+        Args:
+            filename: string, The path of the file to open.
+            mode: string, The mode to try to open the file with.
+            fallback_mode: string, The mode to use if locking fails.
+            use_native_locking: bool, Whether or not fcntl/win32 locking is
+                                used.
+        """
+        opener = None
+        if not opener and use_native_locking:
+            try:
+                from oauth2client.contrib._win32_opener import _Win32Opener
+                opener = _Win32Opener(filename, mode, fallback_mode)
+            except ImportError:
+                try:
+                    from oauth2client.contrib._fcntl_opener import _FcntlOpener
+                    opener = _FcntlOpener(filename, mode, fallback_mode)
+                except ImportError:
+                    pass
+
+        if not opener:
+            opener = _PosixOpener(filename, mode, fallback_mode)
+
+        self._opener = opener
+
+    def filename(self):
+        """Return the filename we were constructed with."""
+        return self._opener._filename
+
+    def file_handle(self):
+        """Return the file_handle to the opened file."""
+        return self._opener.file_handle()
+
+    def is_locked(self):
+        """Return whether we successfully locked the file."""
+        return self._opener.is_locked()
+
+    def open_and_lock(self, timeout=0, delay=0.05):
+        """Open the file, trying to lock it.
+
+        Args:
+            timeout: float, The number of seconds to try to acquire the lock.
+            delay: float, The number of seconds to wait between retry attempts.
+
+        Raises:
+            AlreadyLockedException: if the lock is already acquired.
+            IOError: if the open fails.
+        """
+        self._opener.open_and_lock(timeout, delay)
+
+    def unlock_and_close(self):
+        """Unlock and close a file."""
+        self._opener.unlock_and_close()
diff --git a/oauth2client/contrib/multiprocess_file_storage.py b/oauth2client/contrib/multiprocess_file_storage.py
new file mode 100644
index 0000000..e9e8c8c
--- /dev/null
+++ b/oauth2client/contrib/multiprocess_file_storage.py
@@ -0,0 +1,355 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Multiprocess file credential storage.
+
+This module provides file-based storage that supports multiple credentials and
+cross-thread and process access.
+
+This module supersedes the functionality previously found in `multistore_file`.
+
+This module provides :class:`MultiprocessFileStorage` which:
+    * Is tied to a single credential via a user-specified key. This key can be
+      used to distinguish between multiple users, client ids, and/or scopes.
+    * Can be safely accessed and refreshed across threads and processes.
+
+Process & thread safety guarantees the following behavior:
+    * If one thread or process refreshes a credential, subsequent refreshes
+      from other processes will re-fetch the credentials from the file instead
+      of performing an http request.
+    * If two processes or threads attempt to refresh concurrently, only one
+      will be able to acquire the lock and refresh, with the deadlock caveat
+      below.
+    * The interprocess lock will not deadlock, instead, the if a process can
+      not acquire the interprocess lock within ``INTERPROCESS_LOCK_DEADLINE``
+      it will allow refreshing the credential but will not write the updated
+      credential to disk, This logic happens during every lock cycle - if the
+      credentials are refreshed again it will retry locking and writing as
+      normal.
+
+Usage
+=====
+
+Before using the storage, you need to decide how you want to key the
+credentials. A few common strategies include:
+
+    * If you're storing credentials for multiple users in a single file, use
+      a unique identifier for each user as the key.
+    * If you're storing credentials for multiple client IDs in a single file,
+      use the client ID as the key.
+    * If you're storing multiple credentials for one user, use the scopes as
+      the key.
+    * If you have a complicated setup, use a compound key. For example, you
+      can use a combination of the client ID and scopes as the key.
+
+Create an instance of :class:`MultiprocessFileStorage` for each credential you
+want to store, for example::
+
+    filename = 'credentials'
+    key = '{}-{}'.format(client_id, user_id)
+    storage = MultiprocessFileStorage(filename, key)
+
+To store the credentials::
+
+    storage.put(credentials)
+
+If you're going to continue to use the credentials after storing them, be sure
+to call :func:`set_store`::
+
+    credentials.set_store(storage)
+
+To retrieve the credentials::
+
+    storage.get(credentials)
+
+"""
+
+import base64
+import json
+import logging
+import os
+import threading
+
+import fasteners
+from six import iteritems
+
+from oauth2client import _helpers
+from oauth2client import client
+
+
+#: The maximum amount of time, in seconds, to wait when acquire the
+#: interprocess lock before falling back to read-only mode.
+INTERPROCESS_LOCK_DEADLINE = 1
+
+logger = logging.getLogger(__name__)
+_backends = {}
+_backends_lock = threading.Lock()
+
+
+def _create_file_if_needed(filename):
+    """Creates the an empty file if it does not already exist.
+
+    Returns:
+        True if the file was created, False otherwise.
+    """
+    if os.path.exists(filename):
+        return False
+    else:
+        # Equivalent to "touch".
+        open(filename, 'a+b').close()
+        logger.info('Credential file {0} created'.format(filename))
+        return True
+
+
+def _load_credentials_file(credentials_file):
+    """Load credentials from the given file handle.
+
+    The file is expected to be in this format:
+
+        {
+            "file_version": 2,
+            "credentials": {
+                "key": "base64 encoded json representation of credentials."
+            }
+        }
+
+    This function will warn and return empty credentials instead of raising
+    exceptions.
+
+    Args:
+        credentials_file: An open file handle.
+
+    Returns:
+        A dictionary mapping user-defined keys to an instance of
+        :class:`oauth2client.client.Credentials`.
+    """
+    try:
+        credentials_file.seek(0)
+        data = json.load(credentials_file)
+    except Exception:
+        logger.warning(
+            'Credentials file could not be loaded, will ignore and '
+            'overwrite.')
+        return {}
+
+    if data.get('file_version') != 2:
+        logger.warning(
+            'Credentials file is not version 2, will ignore and '
+            'overwrite.')
+        return {}
+
+    credentials = {}
+
+    for key, encoded_credential in iteritems(data.get('credentials', {})):
+        try:
+            credential_json = base64.b64decode(encoded_credential)
+            credential = client.Credentials.new_from_json(credential_json)
+            credentials[key] = credential
+        except:
+            logger.warning(
+                'Invalid credential {0} in file, ignoring.'.format(key))
+
+    return credentials
+
+
+def _write_credentials_file(credentials_file, credentials):
+    """Writes credentials to a file.
+
+    Refer to :func:`_load_credentials_file` for the format.
+
+    Args:
+        credentials_file: An open file handle, must be read/write.
+        credentials: A dictionary mapping user-defined keys to an instance of
+            :class:`oauth2client.client.Credentials`.
+    """
+    data = {'file_version': 2, 'credentials': {}}
+
+    for key, credential in iteritems(credentials):
+        credential_json = credential.to_json()
+        encoded_credential = _helpers._from_bytes(base64.b64encode(
+            _helpers._to_bytes(credential_json)))
+        data['credentials'][key] = encoded_credential
+
+    credentials_file.seek(0)
+    json.dump(data, credentials_file)
+    credentials_file.truncate()
+
+
+class _MultiprocessStorageBackend(object):
+    """Thread-local backend for multiprocess storage.
+
+    Each process has only one instance of this backend per file. All threads
+    share a single instance of this backend. This ensures that all threads
+    use the same thread lock and process lock when accessing the file.
+    """
+
+    def __init__(self, filename):
+        self._file = None
+        self._filename = filename
+        self._process_lock = fasteners.InterProcessLock(
+            '{0}.lock'.format(filename))
+        self._thread_lock = threading.Lock()
+        self._read_only = False
+        self._credentials = {}
+
+    def _load_credentials(self):
+        """(Re-)loads the credentials from the file."""
+        if not self._file:
+            return
+
+        loaded_credentials = _load_credentials_file(self._file)
+        self._credentials.update(loaded_credentials)
+
+        logger.debug('Read credential file')
+
+    def _write_credentials(self):
+        if self._read_only:
+            logger.debug('In read-only mode, not writing credentials.')
+            return
+
+        _write_credentials_file(self._file, self._credentials)
+        logger.debug('Wrote credential file {0}.'.format(self._filename))
+
+    def acquire_lock(self):
+        self._thread_lock.acquire()
+        locked = self._process_lock.acquire(timeout=INTERPROCESS_LOCK_DEADLINE)
+
+        if locked:
+            _create_file_if_needed(self._filename)
+            self._file = open(self._filename, 'r+')
+            self._read_only = False
+
+        else:
+            logger.warn(
+                'Failed to obtain interprocess lock for credentials. '
+                'If a credential is being refreshed, other processes may '
+                'not see the updated access token and refresh as well.')
+            if os.path.exists(self._filename):
+                self._file = open(self._filename, 'r')
+            else:
+                self._file = None
+            self._read_only = True
+
+        self._load_credentials()
+
+    def release_lock(self):
+        if self._file is not None:
+            self._file.close()
+            self._file = None
+
+        if not self._read_only:
+            self._process_lock.release()
+
+        self._thread_lock.release()
+
+    def _refresh_predicate(self, credentials):
+        if credentials is None:
+            return True
+        elif credentials.invalid:
+            return True
+        elif credentials.access_token_expired:
+            return True
+        else:
+            return False
+
+    def locked_get(self, key):
+        # Check if the credential is already in memory.
+        credentials = self._credentials.get(key, None)
+
+        # Use the refresh predicate to determine if the entire store should be
+        # reloaded. This basically checks if the credentials are invalid
+        # or expired. This covers the situation where another process has
+        # refreshed the credentials and this process doesn't know about it yet.
+        # In that case, this process won't needlessly refresh the credentials.
+        if self._refresh_predicate(credentials):
+            self._load_credentials()
+            credentials = self._credentials.get(key, None)
+
+        return credentials
+
+    def locked_put(self, key, credentials):
+        self._load_credentials()
+        self._credentials[key] = credentials
+        self._write_credentials()
+
+    def locked_delete(self, key):
+        self._load_credentials()
+        self._credentials.pop(key, None)
+        self._write_credentials()
+
+
+def _get_backend(filename):
+    """A helper method to get or create a backend with thread locking.
+
+    This ensures that only one backend is used per-file per-process, so that
+    thread and process locks are appropriately shared.
+
+    Args:
+        filename: The full path to the credential storage file.
+
+    Returns:
+        An instance of :class:`_MultiprocessStorageBackend`.
+    """
+    filename = os.path.abspath(filename)
+
+    with _backends_lock:
+        if filename not in _backends:
+            _backends[filename] = _MultiprocessStorageBackend(filename)
+        return _backends[filename]
+
+
+class MultiprocessFileStorage(client.Storage):
+    """Multiprocess file credential storage.
+
+    Args:
+      filename: The path to the file where credentials will be stored.
+      key: An arbitrary string used to uniquely identify this set of
+          credentials. For example, you may use the user's ID as the key or
+          a combination of the client ID and user ID.
+    """
+    def __init__(self, filename, key):
+        self._key = key
+        self._backend = _get_backend(filename)
+
+    def acquire_lock(self):
+        self._backend.acquire_lock()
+
+    def release_lock(self):
+        self._backend.release_lock()
+
+    def locked_get(self):
+        """Retrieves the current credentials from the store.
+
+        Returns:
+            An instance of :class:`oauth2client.client.Credentials` or `None`.
+        """
+        credential = self._backend.locked_get(self._key)
+
+        if credential is not None:
+            credential.set_store(self)
+
+        return credential
+
+    def locked_put(self, credentials):
+        """Writes the given credentials to the store.
+
+        Args:
+            credentials: an instance of
+                :class:`oauth2client.client.Credentials`.
+        """
+        return self._backend.locked_put(self._key, credentials)
+
+    def locked_delete(self):
+        """Deletes the current credentials from the store."""
+        return self._backend.locked_delete(self._key)
diff --git a/oauth2client/contrib/multistore_file.py b/oauth2client/contrib/multistore_file.py
new file mode 100644
index 0000000..10f4cb4
--- /dev/null
+++ b/oauth2client/contrib/multistore_file.py
@@ -0,0 +1,505 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Multi-credential file store with lock support.
+
+This module implements a JSON credential store where multiple
+credentials can be stored in one file. That file supports locking
+both in a single process and across processes.
+
+The credential themselves are keyed off of:
+
+* client_id
+* user_agent
+* scope
+
+The format of the stored data is like so::
+
+    {
+      'file_version': 1,
+      'data': [
+          {
+              'key': {
+                  'clientId': '<client id>',
+                  'userAgent': '<user agent>',
+                  'scope': '<scope>'
+              },
+              'credential': {
+                  # JSON serialized Credentials.
+              }
+          }
+      ]
+    }
+
+"""
+
+import errno
+import json
+import logging
+import os
+import threading
+
+from oauth2client import client
+from oauth2client import util
+from oauth2client.contrib import locked_file
+
+__author__ = 'jbeda@google.com (Joe Beda)'
+
+logger = logging.getLogger(__name__)
+
+logger.warning(
+    'The oauth2client.contrib.multistore_file module has been deprecated and '
+    'will be removed in the next release of oauth2client. Please migrate to '
+    'multiprocess_file_storage.')
+
+# A dict from 'filename'->_MultiStore instances
+_multistores = {}
+_multistores_lock = threading.Lock()
+
+
+class Error(Exception):
+    """Base error for this module."""
+
+
+class NewerCredentialStoreError(Error):
+    """The credential store is a newer version than supported."""
+
+
+def _dict_to_tuple_key(dictionary):
+    """Converts a dictionary to a tuple that can be used as an immutable key.
+
+    The resulting key is always sorted so that logically equivalent
+    dictionaries always produce an identical tuple for a key.
+
+    Args:
+        dictionary: the dictionary to use as the key.
+
+    Returns:
+        A tuple representing the dictionary in it's naturally sorted ordering.
+    """
+    return tuple(sorted(dictionary.items()))
+
+
+@util.positional(4)
+def get_credential_storage(filename, client_id, user_agent, scope,
+                           warn_on_readonly=True):
+    """Get a Storage instance for a credential.
+
+    Args:
+        filename: The JSON file storing a set of credentials
+        client_id: The client_id for the credential
+        user_agent: The user agent for the credential
+        scope: string or iterable of strings, Scope(s) being requested
+        warn_on_readonly: if True, log a warning if the store is readonly
+
+    Returns:
+        An object derived from client.Storage for getting/setting the
+        credential.
+    """
+    # Recreate the legacy key with these specific parameters
+    key = {'clientId': client_id, 'userAgent': user_agent,
+           'scope': util.scopes_to_string(scope)}
+    return get_credential_storage_custom_key(
+        filename, key, warn_on_readonly=warn_on_readonly)
+
+
+@util.positional(2)
+def get_credential_storage_custom_string_key(filename, key_string,
+                                             warn_on_readonly=True):
+    """Get a Storage instance for a credential using a single string as a key.
+
+    Allows you to provide a string as a custom key that will be used for
+    credential storage and retrieval.
+
+    Args:
+        filename: The JSON file storing a set of credentials
+        key_string: A string to use as the key for storing this credential.
+        warn_on_readonly: if True, log a warning if the store is readonly
+
+    Returns:
+        An object derived from client.Storage for getting/setting the
+        credential.
+    """
+    # Create a key dictionary that can be used
+    key_dict = {'key': key_string}
+    return get_credential_storage_custom_key(
+        filename, key_dict, warn_on_readonly=warn_on_readonly)
+
+
+@util.positional(2)
+def get_credential_storage_custom_key(filename, key_dict,
+                                      warn_on_readonly=True):
+    """Get a Storage instance for a credential using a dictionary as a key.
+
+    Allows you to provide a dictionary as a custom key that will be used for
+    credential storage and retrieval.
+
+    Args:
+        filename: The JSON file storing a set of credentials
+        key_dict: A dictionary to use as the key for storing this credential.
+                  There is no ordering of the keys in the dictionary. Logically
+                  equivalent dictionaries will produce equivalent storage keys.
+        warn_on_readonly: if True, log a warning if the store is readonly
+
+    Returns:
+        An object derived from client.Storage for getting/setting the
+        credential.
+    """
+    multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
+    key = _dict_to_tuple_key(key_dict)
+    return multistore._get_storage(key)
+
+
+@util.positional(1)
+def get_all_credential_keys(filename, warn_on_readonly=True):
+    """Gets all the registered credential keys in the given Multistore.
+
+    Args:
+        filename: The JSON file storing a set of credentials
+        warn_on_readonly: if True, log a warning if the store is readonly
+
+    Returns:
+        A list of the credential keys present in the file.  They are returned
+        as dictionaries that can be passed into
+        get_credential_storage_custom_key to get the actual credentials.
+    """
+    multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
+    multistore._lock()
+    try:
+        return multistore._get_all_credential_keys()
+    finally:
+        multistore._unlock()
+
+
+@util.positional(1)
+def _get_multistore(filename, warn_on_readonly=True):
+    """A helper method to initialize the multistore with proper locking.
+
+    Args:
+        filename: The JSON file storing a set of credentials
+        warn_on_readonly: if True, log a warning if the store is readonly
+
+    Returns:
+        A multistore object
+    """
+    filename = os.path.expanduser(filename)
+    _multistores_lock.acquire()
+    try:
+        multistore = _multistores.setdefault(
+            filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
+    finally:
+        _multistores_lock.release()
+    return multistore
+
+
+class _MultiStore(object):
+    """A file backed store for multiple credentials."""
+
+    @util.positional(2)
+    def __init__(self, filename, warn_on_readonly=True):
+        """Initialize the class.
+
+        This will create the file if necessary.
+        """
+        self._file = locked_file.LockedFile(filename, 'r+', 'r')
+        self._thread_lock = threading.Lock()
+        self._read_only = False
+        self._warn_on_readonly = warn_on_readonly
+
+        self._create_file_if_needed()
+
+        # Cache of deserialized store. This is only valid after the
+        # _MultiStore is locked or _refresh_data_cache is called. This is
+        # of the form of:
+        #
+        # ((key, value), (key, value)...) -> OAuth2Credential
+        #
+        # If this is None, then the store hasn't been read yet.
+        self._data = None
+
+    class _Storage(client.Storage):
+        """A Storage object that can read/write a single credential."""
+
+        def __init__(self, multistore, key):
+            self._multistore = multistore
+            self._key = key
+
+        def acquire_lock(self):
+            """Acquires any lock necessary to access this Storage.
+
+            This lock is not reentrant.
+            """
+            self._multistore._lock()
+
+        def release_lock(self):
+            """Release the Storage lock.
+
+            Trying to release a lock that isn't held will result in a
+            RuntimeError.
+            """
+            self._multistore._unlock()
+
+        def locked_get(self):
+            """Retrieve credential.
+
+            The Storage lock must be held when this is called.
+
+            Returns:
+                oauth2client.client.Credentials
+            """
+            credential = self._multistore._get_credential(self._key)
+            if credential:
+                credential.set_store(self)
+            return credential
+
+        def locked_put(self, credentials):
+            """Write a credential.
+
+            The Storage lock must be held when this is called.
+
+            Args:
+                credentials: Credentials, the credentials to store.
+            """
+            self._multistore._update_credential(self._key, credentials)
+
+        def locked_delete(self):
+            """Delete a credential.
+
+            The Storage lock must be held when this is called.
+
+            Args:
+                credentials: Credentials, the credentials to store.
+            """
+            self._multistore._delete_credential(self._key)
+
+    def _create_file_if_needed(self):
+        """Create an empty file if necessary.
+
+        This method will not initialize the file. Instead it implements a
+        simple version of "touch" to ensure the file has been created.
+        """
+        if not os.path.exists(self._file.filename()):
+            old_umask = os.umask(0o177)
+            try:
+                open(self._file.filename(), 'a+b').close()
+            finally:
+                os.umask(old_umask)
+
+    def _lock(self):
+        """Lock the entire multistore."""
+        self._thread_lock.acquire()
+        try:
+            self._file.open_and_lock()
+        except (IOError, OSError) as e:
+            if e.errno == errno.ENOSYS:
+                logger.warn('File system does not support locking the '
+                            'credentials file.')
+            elif e.errno == errno.ENOLCK:
+                logger.warn('File system is out of resources for writing the '
+                            'credentials file (is your disk full?).')
+            elif e.errno == errno.EDEADLK:
+                logger.warn('Lock contention on multistore file, opening '
+                            'in read-only mode.')
+            elif e.errno == errno.EACCES:
+                logger.warn('Cannot access credentials file.')
+            else:
+                raise
+        if not self._file.is_locked():
+            self._read_only = True
+            if self._warn_on_readonly:
+                logger.warn('The credentials file (%s) is not writable. '
+                            'Opening in read-only mode. Any refreshed '
+                            'credentials will only be '
+                            'valid for this run.', self._file.filename())
+
+        if os.path.getsize(self._file.filename()) == 0:
+            logger.debug('Initializing empty multistore file')
+            # The multistore is empty so write out an empty file.
+            self._data = {}
+            self._write()
+        elif not self._read_only or self._data is None:
+            # Only refresh the data if we are read/write or we haven't
+            # cached the data yet. If we are readonly, we assume is isn't
+            # changing out from under us and that we only have to read it
+            # once. This prevents us from whacking any new access keys that
+            # we have cached in memory but were unable to write out.
+            self._refresh_data_cache()
+
+    def _unlock(self):
+        """Release the lock on the multistore."""
+        self._file.unlock_and_close()
+        self._thread_lock.release()
+
+    def _locked_json_read(self):
+        """Get the raw content of the multistore file.
+
+        The multistore must be locked when this is called.
+
+        Returns:
+            The contents of the multistore decoded as JSON.
+        """
+        assert self._thread_lock.locked()
+        self._file.file_handle().seek(0)
+        return json.load(self._file.file_handle())
+
+    def _locked_json_write(self, data):
+        """Write a JSON serializable data structure to the multistore.
+
+        The multistore must be locked when this is called.
+
+        Args:
+            data: The data to be serialized and written.
+        """
+        assert self._thread_lock.locked()
+        if self._read_only:
+            return
+        self._file.file_handle().seek(0)
+        json.dump(data, self._file.file_handle(),
+                  sort_keys=True, indent=2, separators=(',', ': '))
+        self._file.file_handle().truncate()
+
+    def _refresh_data_cache(self):
+        """Refresh the contents of the multistore.
+
+        The multistore must be locked when this is called.
+
+        Raises:
+            NewerCredentialStoreError: Raised when a newer client has written
+            the store.
+        """
+        self._data = {}
+        try:
+            raw_data = self._locked_json_read()
+        except Exception:
+            logger.warn('Credential data store could not be loaded. '
+                        'Will ignore and overwrite.')
+            return
+
+        version = 0
+        try:
+            version = raw_data['file_version']
+        except Exception:
+            logger.warn('Missing version for credential data store. It may be '
+                        'corrupt or an old version. Overwriting.')
+        if version > 1:
+            raise NewerCredentialStoreError(
+                'Credential file has file_version of {0}. '
+                'Only file_version of 1 is supported.'.format(version))
+
+        credentials = []
+        try:
+            credentials = raw_data['data']
+        except (TypeError, KeyError):
+            pass
+
+        for cred_entry in credentials:
+            try:
+                key, credential = self._decode_credential_from_json(cred_entry)
+                self._data[key] = credential
+            except:
+                # If something goes wrong loading a credential, just ignore it
+                logger.info('Error decoding credential, skipping',
+                            exc_info=True)
+
+    def _decode_credential_from_json(self, cred_entry):
+        """Load a credential from our JSON serialization.
+
+        Args:
+            cred_entry: A dict entry from the data member of our format
+
+        Returns:
+            (key, cred) where the key is the key tuple and the cred is the
+            OAuth2Credential object.
+        """
+        raw_key = cred_entry['key']
+        key = _dict_to_tuple_key(raw_key)
+        credential = None
+        credential = client.Credentials.new_from_json(
+            json.dumps(cred_entry['credential']))
+        return (key, credential)
+
+    def _write(self):
+        """Write the cached data back out.
+
+        The multistore must be locked.
+        """
+        raw_data = {'file_version': 1}
+        raw_creds = []
+        raw_data['data'] = raw_creds
+        for (cred_key, cred) in self._data.items():
+            raw_key = dict(cred_key)
+            raw_cred = json.loads(cred.to_json())
+            raw_creds.append({'key': raw_key, 'credential': raw_cred})
+        self._locked_json_write(raw_data)
+
+    def _get_all_credential_keys(self):
+        """Gets all the registered credential keys in the multistore.
+
+        Returns:
+            A list of dictionaries corresponding to all the keys currently
+            registered
+        """
+        return [dict(key) for key in self._data.keys()]
+
+    def _get_credential(self, key):
+        """Get a credential from the multistore.
+
+        The multistore must be locked.
+
+        Args:
+            key: The key used to retrieve the credential
+
+        Returns:
+            The credential specified or None if not present
+        """
+        return self._data.get(key, None)
+
+    def _update_credential(self, key, cred):
+        """Update a credential and write the multistore.
+
+        This must be called when the multistore is locked.
+
+        Args:
+            key: The key used to retrieve the credential
+            cred: The OAuth2Credential to update/set
+        """
+        self._data[key] = cred
+        self._write()
+
+    def _delete_credential(self, key):
+        """Delete a credential and write the multistore.
+
+        This must be called when the multistore is locked.
+
+        Args:
+            key: The key used to retrieve the credential
+        """
+        try:
+            del self._data[key]
+        except KeyError:
+            pass
+        self._write()
+
+    def _get_storage(self, key):
+        """Get a Storage object to get/set a credential.
+
+        This Storage is a 'view' into the multistore.
+
+        Args:
+            key: The key used to retrieve the credential
+
+        Returns:
+            A Storage object that can be used to get/set this cred
+        """
+        return self._Storage(self, key)
diff --git a/oauth2client/contrib/sqlalchemy.py b/oauth2client/contrib/sqlalchemy.py
new file mode 100644
index 0000000..7d9fd4b
--- /dev/null
+++ b/oauth2client/contrib/sqlalchemy.py
@@ -0,0 +1,173 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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 utilities for SQLAlchemy.
+
+Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy.
+
+Configuration
+=============
+
+In order to use this storage, you'll need to create table
+with :class:`oauth2client.contrib.sqlalchemy.CredentialsType` column.
+It's recommended to either put this column on some sort of user info
+table or put the column in a table with a belongs-to relationship to
+a user info table.
+
+Here's an example of a simple table with a :class:`CredentialsType`
+column that's related to a user table by the `user_id` key.
+
+.. code-block:: python
+
+    from sqlalchemy import Column, ForeignKey, Integer
+    from sqlalchemy.ext.declarative import declarative_base
+    from sqlalchemy.orm import relationship
+
+    from oauth2client.contrib.sqlalchemy import CredentialsType
+
+
+    Base = declarative_base()
+
+
+    class Credentials(Base):
+        __tablename__ = 'credentials'
+
+        user_id = Column(Integer, ForeignKey('user.id'))
+        credentials = Column(CredentialsType)
+
+
+    class User(Base):
+        id = Column(Integer, primary_key=True)
+        # bunch of other columns
+        credentials = relationship('Credentials')
+
+
+Usage
+=====
+
+With tables ready, you are now able to store credentials in database.
+We will reuse tables defined above.
+
+.. code-block:: python
+
+    from sqlalchemy.orm import Session
+
+    from oauth2client.client import OAuth2Credentials
+    from oauth2client.contrib.sql_alchemy import Storage
+
+    session = Session()
+    user = session.query(User).first()
+    storage = Storage(
+        session=session,
+        model_class=Credentials,
+        # This is the key column used to identify
+        # the row that stores the credentials.
+        key_name='user_id',
+        key_value=user.id,
+        property_name='credentials',
+    )
+
+    # Store
+    credentials = OAuth2Credentials(...)
+    storage.put(credentials)
+
+    # Retrieve
+    credentials = storage.get()
+
+    # Delete
+    storage.delete()
+
+"""
+
+from __future__ import absolute_import
+
+import sqlalchemy.types
+
+from oauth2client import client
+
+
+class CredentialsType(sqlalchemy.types.PickleType):
+    """Type representing credentials.
+
+    Alias for :class:`sqlalchemy.types.PickleType`.
+    """
+
+
+class Storage(client.Storage):
+    """Store and retrieve a single credential to and from SQLAlchemy.
+    This helper presumes the Credentials
+    have been stored as a Credentials column
+    on a db model class.
+    """
+
+    def __init__(self, session, model_class, key_name,
+                 key_value, property_name):
+        """Constructor for Storage.
+
+        Args:
+            session: An instance of :class:`sqlalchemy.orm.Session`.
+            model_class: SQLAlchemy declarative mapping.
+            key_name: string, key name for the entity that has the credentials
+            key_value: key value for the entity that has the credentials
+            property_name: A string indicating which property on the
+                           ``model_class`` to store the credentials.
+                           This property must be a
+                           :class:`CredentialsType` column.
+        """
+        super(Storage, self).__init__()
+
+        self.session = session
+        self.model_class = model_class
+        self.key_name = key_name
+        self.key_value = key_value
+        self.property_name = property_name
+
+    def locked_get(self):
+        """Retrieve stored credential.
+
+        Returns:
+            A :class:`oauth2client.Credentials` instance or `None`.
+        """
+        filters = {self.key_name: self.key_value}
+        query = self.session.query(self.model_class).filter_by(**filters)
+        entity = query.first()
+
+        if entity:
+            credential = getattr(entity, self.property_name)
+            if credential and hasattr(credential, 'set_store'):
+                credential.set_store(self)
+            return credential
+        else:
+            return None
+
+    def locked_put(self, credentials):
+        """Write a credentials to the SQLAlchemy datastore.
+
+        Args:
+            credentials: :class:`oauth2client.Credentials`
+        """
+        filters = {self.key_name: self.key_value}
+        query = self.session.query(self.model_class).filter_by(**filters)
+        entity = query.first()
+
+        if not entity:
+            entity = self.model_class(**filters)
+
+        setattr(entity, self.property_name, credentials)
+        self.session.add(entity)
+
+    def locked_delete(self):
+        """Delete credentials from the SQLAlchemy datastore."""
+        filters = {self.key_name: self.key_value}
+        self.session.query(self.model_class).filter_by(**filters).delete()
diff --git a/oauth2client/contrib/xsrfutil.py b/oauth2client/contrib/xsrfutil.py
new file mode 100644
index 0000000..c03e679
--- /dev/null
+++ b/oauth2client/contrib/xsrfutil.py
@@ -0,0 +1,106 @@
+# Copyright 2014 the Melange authors.
+#
+# 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.
+
+"""Helper methods for creating & verifying XSRF tokens."""
+
+import base64
+import binascii
+import hmac
+import time
+
+from oauth2client import _helpers
+from oauth2client import util
+
+__authors__ = [
+    '"Doug Coker" <dcoker@google.com>',
+    '"Joe Gregorio" <jcgregorio@google.com>',
+]
+
+# Delimiter character
+DELIMITER = b':'
+
+# 1 hour in seconds
+DEFAULT_TIMEOUT_SECS = 60 * 60
+
+
+@util.positional(2)
+def generate_token(key, user_id, action_id='', when=None):
+    """Generates a URL-safe token for the given user, action, time tuple.
+
+    Args:
+        key: secret key to use.
+        user_id: the user ID of the authenticated user.
+        action_id: a string identifier of the action they requested
+                   authorization for.
+        when: the time in seconds since the epoch at which the user was
+              authorized for this action. If not set the current time is used.
+
+    Returns:
+        A string XSRF protection token.
+    """
+    digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8'))
+    digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8'))
+    digester.update(DELIMITER)
+    digester.update(_helpers._to_bytes(action_id, encoding='utf-8'))
+    digester.update(DELIMITER)
+    when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8')
+    digester.update(when)
+    digest = digester.digest()
+
+    token = base64.urlsafe_b64encode(digest + DELIMITER + when)
+    return token
+
+
+@util.positional(3)
+def validate_token(key, token, user_id, action_id="", current_time=None):
+    """Validates that the given token authorizes the user for the action.
+
+    Tokens are invalid if the time of issue is too old or if the token
+    does not match what generateToken outputs (i.e. the token was forged).
+
+    Args:
+        key: secret key to use.
+        token: a string of the token generated by generateToken.
+        user_id: the user ID of the authenticated user.
+        action_id: a string identifier of the action they requested
+                   authorization for.
+
+    Returns:
+        A boolean - True if the user is authorized for the action, False
+        otherwise.
+    """
+    if not token:
+        return False
+    try:
+        decoded = base64.urlsafe_b64decode(token)
+        token_time = int(decoded.split(DELIMITER)[-1])
+    except (TypeError, ValueError, binascii.Error):
+        return False
+    if current_time is None:
+        current_time = time.time()
+    # If the token is too old it's not valid.
+    if current_time - token_time > DEFAULT_TIMEOUT_SECS:
+        return False
+
+    # The given token should match the generated one with the same time.
+    expected_token = generate_token(key, user_id, action_id=action_id,
+                                    when=token_time)
+    if len(token) != len(expected_token):
+        return False
+
+    # Perform constant time comparison to avoid timing attacks
+    different = 0
+    for x, y in zip(bytearray(token), bytearray(expected_token)):
+        different |= x ^ y
+    return not different
diff --git a/oauth2client/crypt.py b/oauth2client/crypt.py
new file mode 100644
index 0000000..1326098
--- /dev/null
+++ b/oauth2client/crypt.py
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+"""Crypto-related routines for oauth2client."""
+
+import json
+import logging
+import time
+
+from oauth2client import _helpers
+from oauth2client import _pure_python_crypt
+
+
+RsaSigner = _pure_python_crypt.RsaSigner
+RsaVerifier = _pure_python_crypt.RsaVerifier
+
+CLOCK_SKEW_SECS = 300  # 5 minutes in seconds
+AUTH_TOKEN_LIFETIME_SECS = 300  # 5 minutes in seconds
+MAX_TOKEN_LIFETIME_SECS = 86400  # 1 day in seconds
+
+logger = logging.getLogger(__name__)
+
+
+class AppIdentityError(Exception):
+    """Error to indicate crypto failure."""
+
+
+def _bad_pkcs12_key_as_pem(*args, **kwargs):
+    raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
+
+
+try:
+    from oauth2client import _openssl_crypt
+    OpenSSLSigner = _openssl_crypt.OpenSSLSigner
+    OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
+    pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
+except ImportError:  # pragma: NO COVER
+    OpenSSLVerifier = None
+    OpenSSLSigner = None
+    pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
+
+try:
+    from oauth2client import _pycrypto_crypt
+    PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
+    PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
+except ImportError:  # pragma: NO COVER
+    PyCryptoVerifier = None
+    PyCryptoSigner = None
+
+
+if OpenSSLSigner:
+    Signer = OpenSSLSigner
+    Verifier = OpenSSLVerifier
+elif PyCryptoSigner:  # pragma: NO COVER
+    Signer = PyCryptoSigner
+    Verifier = PyCryptoVerifier
+else:  # pragma: NO COVER
+    Signer = RsaSigner
+    Verifier = RsaVerifier
+
+
+def make_signed_jwt(signer, payload, key_id=None):
+    """Make a signed JWT.
+
+    See http://self-issued.info/docs/draft-jones-json-web-token.html.
+
+    Args:
+        signer: crypt.Signer, Cryptographic signer.
+        payload: dict, Dictionary of data to convert to JSON and then sign.
+        key_id: string, (Optional) Key ID header.
+
+    Returns:
+        string, The JWT for the payload.
+    """
+    header = {'typ': 'JWT', 'alg': 'RS256'}
+    if key_id is not None:
+        header['kid'] = key_id
+
+    segments = [
+        _helpers._urlsafe_b64encode(_helpers._json_encode(header)),
+        _helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
+    ]
+    signing_input = b'.'.join(segments)
+
+    signature = signer.sign(signing_input)
+    segments.append(_helpers._urlsafe_b64encode(signature))
+
+    logger.debug(str(segments))
+
+    return b'.'.join(segments)
+
+
+def _verify_signature(message, signature, certs):
+    """Verifies signed content using a list of certificates.
+
+    Args:
+        message: string or bytes, The message to verify.
+        signature: string or bytes, The signature on the message.
+        certs: iterable, certificates in PEM format.
+
+    Raises:
+        AppIdentityError: If none of the certificates can verify the message
+                          against the signature.
+    """
+    for pem in certs:
+        verifier = Verifier.from_string(pem, is_x509_cert=True)
+        if verifier.verify(message, signature):
+            return
+
+    # If we have not returned, no certificate confirms the signature.
+    raise AppIdentityError('Invalid token signature')
+
+
+def _check_audience(payload_dict, audience):
+    """Checks audience field from a JWT payload.
+
+    Does nothing if the passed in ``audience`` is null.
+
+    Args:
+        payload_dict: dict, A dictionary containing a JWT payload.
+        audience: string or NoneType, an audience to check for in
+                  the JWT payload.
+
+    Raises:
+        AppIdentityError: If there is no ``'aud'`` field in the payload
+                          dictionary but there is an ``audience`` to check.
+        AppIdentityError: If the ``'aud'`` field in the payload dictionary
+                          does not match the ``audience``.
+    """
+    if audience is None:
+        return
+
+    audience_in_payload = payload_dict.get('aud')
+    if audience_in_payload is None:
+        raise AppIdentityError(
+            'No aud field in token: {0}'.format(payload_dict))
+    if audience_in_payload != audience:
+        raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
+            audience_in_payload, audience, payload_dict))
+
+
+def _verify_time_range(payload_dict):
+    """Verifies the issued at and expiration from a JWT payload.
+
+    Makes sure the current time (in UTC) falls between the issued at and
+    expiration for the JWT (with some skew allowed for via
+    ``CLOCK_SKEW_SECS``).
+
+    Args:
+        payload_dict: dict, A dictionary containing a JWT payload.
+
+    Raises:
+        AppIdentityError: If there is no ``'iat'`` field in the payload
+                          dictionary.
+        AppIdentityError: If there is no ``'exp'`` field in the payload
+                          dictionary.
+        AppIdentityError: If the JWT expiration is too far in the future (i.e.
+                          if the expiration would imply a token lifetime
+                          longer than what is allowed.)
+        AppIdentityError: If the token appears to have been issued in the
+                          future (up to clock skew).
+        AppIdentityError: If the token appears to have expired in the past
+                          (up to clock skew).
+    """
+    # Get the current time to use throughout.
+    now = int(time.time())
+
+    # Make sure issued at and expiration are in the payload.
+    issued_at = payload_dict.get('iat')
+    if issued_at is None:
+        raise AppIdentityError(
+            'No iat field in token: {0}'.format(payload_dict))
+    expiration = payload_dict.get('exp')
+    if expiration is None:
+        raise AppIdentityError(
+            'No exp field in token: {0}'.format(payload_dict))
+
+    # Make sure the expiration gives an acceptable token lifetime.
+    if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
+        raise AppIdentityError(
+            'exp field too far in future: {0}'.format(payload_dict))
+
+    # Make sure (up to clock skew) that the token wasn't issued in the future.
+    earliest = issued_at - CLOCK_SKEW_SECS
+    if now < earliest:
+        raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
+            now, earliest, payload_dict))
+    # Make sure (up to clock skew) that the token isn't already expired.
+    latest = expiration + CLOCK_SKEW_SECS
+    if now > latest:
+        raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
+            now, latest, payload_dict))
+
+
+def verify_signed_jwt_with_certs(jwt, certs, audience=None):
+    """Verify a JWT against public certs.
+
+    See http://self-issued.info/docs/draft-jones-json-web-token.html.
+
+    Args:
+        jwt: string, A JWT.
+        certs: dict, Dictionary where values of public keys in PEM format.
+        audience: string, The audience, 'aud', that this JWT should contain. If
+                  None then the JWT's 'aud' parameter is not verified.
+
+    Returns:
+        dict, The deserialized JSON payload in the JWT.
+
+    Raises:
+        AppIdentityError: if any checks are failed.
+    """
+    jwt = _helpers._to_bytes(jwt)
+
+    if jwt.count(b'.') != 2:
+        raise AppIdentityError(
+            'Wrong number of segments in token: {0}'.format(jwt))
+
+    header, payload, signature = jwt.split(b'.')
+    message_to_sign = header + b'.' + payload
+    signature = _helpers._urlsafe_b64decode(signature)
+
+    # Parse token.
+    payload_bytes = _helpers._urlsafe_b64decode(payload)
+    try:
+        payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
+    except:
+        raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
+
+    # Verify that the signature matches the message.
+    _verify_signature(message_to_sign, signature, certs.values())
+
+    # Verify the issued at and created times in the payload.
+    _verify_time_range(payload_dict)
+
+    # Check audience.
+    _check_audience(payload_dict, audience)
+
+    return payload_dict
diff --git a/oauth2client/file.py b/oauth2client/file.py
new file mode 100644
index 0000000..feede11
--- /dev/null
+++ b/oauth2client/file.py
@@ -0,0 +1,106 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Utilities for OAuth.
+
+Utilities for making it easier to work with OAuth 2.0
+credentials.
+"""
+
+import os
+import threading
+
+from oauth2client import client
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class CredentialsFileSymbolicLinkError(Exception):
+    """Credentials files must not be symbolic links."""
+
+
+class Storage(client.Storage):
+    """Store and retrieve a single credential to and from a file."""
+
+    def __init__(self, filename):
+        super(Storage, self).__init__(lock=threading.Lock())
+        self._filename = filename
+
+    def _validate_file(self):
+        if os.path.islink(self._filename):
+            raise CredentialsFileSymbolicLinkError(
+                'File: {0} is a symbolic link.'.format(self._filename))
+
+    def locked_get(self):
+        """Retrieve Credential from file.
+
+        Returns:
+            oauth2client.client.Credentials
+
+        Raises:
+            CredentialsFileSymbolicLinkError if the file is a symbolic link.
+        """
+        credentials = None
+        self._validate_file()
+        try:
+            f = open(self._filename, 'rb')
+            content = f.read()
+            f.close()
+        except IOError:
+            return credentials
+
+        try:
+            credentials = client.Credentials.new_from_json(content)
+            credentials.set_store(self)
+        except ValueError:
+            pass
+
+        return credentials
+
+    def _create_file_if_needed(self):
+        """Create an empty file if necessary.
+
+        This method will not initialize the file. Instead it implements a
+        simple version of "touch" to ensure the file has been created.
+        """
+        if not os.path.exists(self._filename):
+            old_umask = os.umask(0o177)
+            try:
+                open(self._filename, 'a+b').close()
+            finally:
+                os.umask(old_umask)
+
+    def locked_put(self, credentials):
+        """Write Credentials to file.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+
+        Raises:
+            CredentialsFileSymbolicLinkError if the file is a symbolic link.
+        """
+        self._create_file_if_needed()
+        self._validate_file()
+        f = open(self._filename, 'w')
+        f.write(credentials.to_json())
+        f.close()
+
+    def locked_delete(self):
+        """Delete Credentials file.
+
+        Args:
+            credentials: Credentials, the credentials to store.
+        """
+        os.unlink(self._filename)
diff --git a/oauth2client/service_account.py b/oauth2client/service_account.py
new file mode 100644
index 0000000..bdcfd69
--- /dev/null
+++ b/oauth2client/service_account.py
@@ -0,0 +1,673 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""oauth2client Service account credentials class."""
+
+import base64
+import copy
+import datetime
+import json
+import time
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import crypt
+from oauth2client import transport
+from oauth2client import util
+
+
+_PASSWORD_DEFAULT = 'notasecret'
+_PKCS12_KEY = '_private_key_pkcs12'
+_PKCS12_ERROR = r"""
+This library only implements PKCS#12 support via the pyOpenSSL library.
+Either install pyOpenSSL, or please convert the .p12 file
+to .pem format:
+    $ cat key.p12 | \
+    >   openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
+    >   openssl rsa > key.pem
+"""
+
+
+class ServiceAccountCredentials(client.AssertionCredentials):
+    """Service Account credential for OAuth 2.0 signed JWT grants.
+
+    Supports
+
+    * JSON keyfile (typically contains a PKCS8 key stored as
+      PEM text)
+    * ``.p12`` key (stores PKCS12 key and certificate)
+
+    Makes an assertion to server using a signed JWT assertion in exchange
+    for an access token.
+
+    This credential does not require a flow to instantiate because it
+    represents a two legged flow, and therefore has all of the required
+    information to generate and refresh its own access tokens.
+
+    Args:
+        service_account_email: string, The email associated with the
+                               service account.
+        signer: ``crypt.Signer``, A signer which can be used to sign content.
+        scopes: List or string, (Optional) Scopes to use when acquiring
+                an access token.
+        private_key_id: string, (Optional) Private key identifier. Typically
+                        only used with a JSON keyfile. Can be sent in the
+                        header of a JWT token assertion.
+        client_id: string, (Optional) Client ID for the project that owns the
+                   service account.
+        user_agent: string, (Optional) User agent to use when sending
+                    request.
+        token_uri: string, URI for token endpoint. For convenience defaults
+                   to Google's endpoints but any OAuth 2.0 provider can be
+                   used.
+        revoke_uri: string, URI for revoke endpoint.  For convenience defaults
+                   to Google's endpoints but any OAuth 2.0 provider can be
+                   used.
+        kwargs: dict, Extra key-value pairs (both strings) to send in the
+                payload body when making an assertion.
+    """
+
+    MAX_TOKEN_LIFETIME_SECS = 3600
+    """Max lifetime of the token (one hour, in seconds)."""
+
+    NON_SERIALIZED_MEMBERS = (
+        frozenset(['_signer']) |
+        client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
+    """Members that aren't serialized when object is converted to JSON."""
+
+    # Can be over-ridden by factory constructors. Used for
+    # serialization/deserialization purposes.
+    _private_key_pkcs8_pem = None
+    _private_key_pkcs12 = None
+    _private_key_password = None
+
+    def __init__(self,
+                 service_account_email,
+                 signer,
+                 scopes='',
+                 private_key_id=None,
+                 client_id=None,
+                 user_agent=None,
+                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+                 **kwargs):
+
+        super(ServiceAccountCredentials, self).__init__(
+            None, user_agent=user_agent, token_uri=token_uri,
+            revoke_uri=revoke_uri)
+
+        self._service_account_email = service_account_email
+        self._signer = signer
+        self._scopes = util.scopes_to_string(scopes)
+        self._private_key_id = private_key_id
+        self.client_id = client_id
+        self._user_agent = user_agent
+        self._kwargs = kwargs
+
+    def _to_json(self, strip, to_serialize=None):
+        """Utility function that creates JSON repr. of a credentials object.
+
+        Over-ride is needed since PKCS#12 keys will not in general be JSON
+        serializable.
+
+        Args:
+            strip: array, An array of names of members to exclude from the
+                   JSON.
+            to_serialize: dict, (Optional) The properties for this object
+                          that will be serialized. This allows callers to
+                          modify before serializing.
+
+        Returns:
+            string, a JSON representation of this instance, suitable to pass to
+            from_json().
+        """
+        if to_serialize is None:
+            to_serialize = copy.copy(self.__dict__)
+        pkcs12_val = to_serialize.get(_PKCS12_KEY)
+        if pkcs12_val is not None:
+            to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
+        return super(ServiceAccountCredentials, self)._to_json(
+            strip, to_serialize=to_serialize)
+
+    @classmethod
+    def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
+                                  token_uri=None, revoke_uri=None):
+        """Helper for factory constructors from JSON keyfile.
+
+        Args:
+            keyfile_dict: dict-like object, The parsed dictionary-like object
+                          containing the contents of the JSON keyfile.
+            scopes: List or string, Scopes to use when acquiring an
+                    access token.
+            token_uri: string, URI for OAuth 2.0 provider token endpoint.
+                       If unset and not present in keyfile_dict, defaults
+                       to Google's endpoints.
+            revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
+                       If unset and not present in keyfile_dict, defaults
+                       to Google's endpoints.
+
+        Returns:
+            ServiceAccountCredentials, a credentials object created from
+            the keyfile contents.
+
+        Raises:
+            ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
+            KeyError, if one of the expected keys is not present in
+                the keyfile.
+        """
+        creds_type = keyfile_dict.get('type')
+        if creds_type != client.SERVICE_ACCOUNT:
+            raise ValueError('Unexpected credentials type', creds_type,
+                             'Expected', client.SERVICE_ACCOUNT)
+
+        service_account_email = keyfile_dict['client_email']
+        private_key_pkcs8_pem = keyfile_dict['private_key']
+        private_key_id = keyfile_dict['private_key_id']
+        client_id = keyfile_dict['client_id']
+        if not token_uri:
+            token_uri = keyfile_dict.get('token_uri',
+                                         oauth2client.GOOGLE_TOKEN_URI)
+        if not revoke_uri:
+            revoke_uri = keyfile_dict.get('revoke_uri',
+                                          oauth2client.GOOGLE_REVOKE_URI)
+
+        signer = crypt.Signer.from_string(private_key_pkcs8_pem)
+        credentials = cls(service_account_email, signer, scopes=scopes,
+                          private_key_id=private_key_id,
+                          client_id=client_id, token_uri=token_uri,
+                          revoke_uri=revoke_uri)
+        credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
+        return credentials
+
+    @classmethod
+    def from_json_keyfile_name(cls, filename, scopes='',
+                               token_uri=None, revoke_uri=None):
+
+        """Factory constructor from JSON keyfile by name.
+
+        Args:
+            filename: string, The location of the keyfile.
+            scopes: List or string, (Optional) Scopes to use when acquiring an
+                    access token.
+            token_uri: string, URI for OAuth 2.0 provider token endpoint.
+                       If unset and not present in the key file, defaults
+                       to Google's endpoints.
+            revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
+                       If unset and not present in the key file, defaults
+                       to Google's endpoints.
+
+        Returns:
+            ServiceAccountCredentials, a credentials object created from
+            the keyfile.
+
+        Raises:
+            ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
+            KeyError, if one of the expected keys is not present in
+                the keyfile.
+        """
+        with open(filename, 'r') as file_obj:
+            client_credentials = json.load(file_obj)
+        return cls._from_parsed_json_keyfile(client_credentials, scopes,
+                                             token_uri=token_uri,
+                                             revoke_uri=revoke_uri)
+
+    @classmethod
+    def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
+                               token_uri=None, revoke_uri=None):
+        """Factory constructor from parsed JSON keyfile.
+
+        Args:
+            keyfile_dict: dict-like object, The parsed dictionary-like object
+                          containing the contents of the JSON keyfile.
+            scopes: List or string, (Optional) Scopes to use when acquiring an
+                    access token.
+            token_uri: string, URI for OAuth 2.0 provider token endpoint.
+                       If unset and not present in keyfile_dict, defaults
+                       to Google's endpoints.
+            revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
+                       If unset and not present in keyfile_dict, defaults
+                       to Google's endpoints.
+
+        Returns:
+            ServiceAccountCredentials, a credentials object created from
+            the keyfile.
+
+        Raises:
+            ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
+            KeyError, if one of the expected keys is not present in
+                the keyfile.
+        """
+        return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
+                                             token_uri=token_uri,
+                                             revoke_uri=revoke_uri)
+
+    @classmethod
+    def _from_p12_keyfile_contents(cls, service_account_email,
+                                   private_key_pkcs12,
+                                   private_key_password=None, scopes='',
+                                   token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                                   revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+        """Factory constructor from JSON keyfile.
+
+        Args:
+            service_account_email: string, The email associated with the
+                                   service account.
+            private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
+            private_key_password: string, (Optional) Password for PKCS#12
+                                  private key. Defaults to ``notasecret``.
+            scopes: List or string, (Optional) Scopes to use when acquiring an
+                    access token.
+            token_uri: string, URI for token endpoint. For convenience defaults
+                       to Google's endpoints but any OAuth 2.0 provider can be
+                       used.
+            revoke_uri: string, URI for revoke endpoint. For convenience
+                        defaults to Google's endpoints but any OAuth 2.0
+                        provider can be used.
+
+        Returns:
+            ServiceAccountCredentials, a credentials object created from
+            the keyfile.
+
+        Raises:
+            NotImplementedError if pyOpenSSL is not installed / not the
+            active crypto library.
+        """
+        if private_key_password is None:
+            private_key_password = _PASSWORD_DEFAULT
+        if crypt.Signer is not crypt.OpenSSLSigner:
+            raise NotImplementedError(_PKCS12_ERROR)
+        signer = crypt.Signer.from_string(private_key_pkcs12,
+                                          private_key_password)
+        credentials = cls(service_account_email, signer, scopes=scopes,
+                          token_uri=token_uri, revoke_uri=revoke_uri)
+        credentials._private_key_pkcs12 = private_key_pkcs12
+        credentials._private_key_password = private_key_password
+        return credentials
+
+    @classmethod
+    def from_p12_keyfile(cls, service_account_email, filename,
+                         private_key_password=None, scopes='',
+                         token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                         revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+
+        """Factory constructor from JSON keyfile.
+
+        Args:
+            service_account_email: string, The email associated with the
+                                   service account.
+            filename: string, The location of the PKCS#12 keyfile.
+            private_key_password: string, (Optional) Password for PKCS#12
+                                  private key. Defaults to ``notasecret``.
+            scopes: List or string, (Optional) Scopes to use when acquiring an
+                    access token.
+            token_uri: string, URI for token endpoint. For convenience defaults
+                       to Google's endpoints but any OAuth 2.0 provider can be
+                       used.
+            revoke_uri: string, URI for revoke endpoint. For convenience
+                        defaults to Google's endpoints but any OAuth 2.0
+                        provider can be used.
+
+        Returns:
+            ServiceAccountCredentials, a credentials object created from
+            the keyfile.
+
+        Raises:
+            NotImplementedError if pyOpenSSL is not installed / not the
+            active crypto library.
+        """
+        with open(filename, 'rb') as file_obj:
+            private_key_pkcs12 = file_obj.read()
+        return cls._from_p12_keyfile_contents(
+            service_account_email, private_key_pkcs12,
+            private_key_password=private_key_password, scopes=scopes,
+            token_uri=token_uri, revoke_uri=revoke_uri)
+
+    @classmethod
+    def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
+                                private_key_password=None, scopes='',
+                                token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                                revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+        """Factory constructor from JSON keyfile.
+
+        Args:
+            service_account_email: string, The email associated with the
+                                   service account.
+            file_buffer: stream, A buffer that implements ``read()``
+                         and contains the PKCS#12 key contents.
+            private_key_password: string, (Optional) Password for PKCS#12
+                                  private key. Defaults to ``notasecret``.
+            scopes: List or string, (Optional) Scopes to use when acquiring an
+                    access token.
+            token_uri: string, URI for token endpoint. For convenience defaults
+                       to Google's endpoints but any OAuth 2.0 provider can be
+                       used.
+            revoke_uri: string, URI for revoke endpoint. For convenience
+                        defaults to Google's endpoints but any OAuth 2.0
+                        provider can be used.
+
+        Returns:
+            ServiceAccountCredentials, a credentials object created from
+            the keyfile.
+
+        Raises:
+            NotImplementedError if pyOpenSSL is not installed / not the
+            active crypto library.
+        """
+        private_key_pkcs12 = file_buffer.read()
+        return cls._from_p12_keyfile_contents(
+            service_account_email, private_key_pkcs12,
+            private_key_password=private_key_password, scopes=scopes,
+            token_uri=token_uri, revoke_uri=revoke_uri)
+
+    def _generate_assertion(self):
+        """Generate the assertion that will be used in the request."""
+        now = int(time.time())
+        payload = {
+            'aud': self.token_uri,
+            'scope': self._scopes,
+            'iat': now,
+            'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
+            'iss': self._service_account_email,
+        }
+        payload.update(self._kwargs)
+        return crypt.make_signed_jwt(self._signer, payload,
+                                     key_id=self._private_key_id)
+
+    def sign_blob(self, blob):
+        """Cryptographically sign a blob (of bytes).
+
+        Implements abstract method
+        :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
+
+        Args:
+            blob: bytes, Message to be signed.
+
+        Returns:
+            tuple, A pair of the private key ID used to sign the blob and
+            the signed contents.
+        """
+        return self._private_key_id, self._signer.sign(blob)
+
+    @property
+    def service_account_email(self):
+        """Get the email for the current service account.
+
+        Returns:
+            string, The email associated with the service account.
+        """
+        return self._service_account_email
+
+    @property
+    def serialization_data(self):
+        # NOTE: This is only useful for JSON keyfile.
+        return {
+            'type': 'service_account',
+            'client_email': self._service_account_email,
+            'private_key_id': self._private_key_id,
+            'private_key': self._private_key_pkcs8_pem,
+            'client_id': self.client_id,
+        }
+
+    @classmethod
+    def from_json(cls, json_data):
+        """Deserialize a JSON-serialized instance.
+
+        Inverse to :meth:`to_json`.
+
+        Args:
+            json_data: dict or string, Serialized JSON (as a string or an
+                       already parsed dictionary) representing a credential.
+
+        Returns:
+            ServiceAccountCredentials from the serialized data.
+        """
+        if not isinstance(json_data, dict):
+            json_data = json.loads(_helpers._from_bytes(json_data))
+
+        private_key_pkcs8_pem = None
+        pkcs12_val = json_data.get(_PKCS12_KEY)
+        password = None
+        if pkcs12_val is None:
+            private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
+            signer = crypt.Signer.from_string(private_key_pkcs8_pem)
+        else:
+            # NOTE: This assumes that private_key_pkcs8_pem is not also
+            #       in the serialized data. This would be very incorrect
+            #       state.
+            pkcs12_val = base64.b64decode(pkcs12_val)
+            password = json_data['_private_key_password']
+            signer = crypt.Signer.from_string(pkcs12_val, password)
+
+        credentials = cls(
+            json_data['_service_account_email'],
+            signer,
+            scopes=json_data['_scopes'],
+            private_key_id=json_data['_private_key_id'],
+            client_id=json_data['client_id'],
+            user_agent=json_data['_user_agent'],
+            **json_data['_kwargs']
+        )
+        if private_key_pkcs8_pem is not None:
+            credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
+        if pkcs12_val is not None:
+            credentials._private_key_pkcs12 = pkcs12_val
+        if password is not None:
+            credentials._private_key_password = password
+        credentials.invalid = json_data['invalid']
+        credentials.access_token = json_data['access_token']
+        credentials.token_uri = json_data['token_uri']
+        credentials.revoke_uri = json_data['revoke_uri']
+        token_expiry = json_data.get('token_expiry', None)
+        if token_expiry is not None:
+            credentials.token_expiry = datetime.datetime.strptime(
+                token_expiry, client.EXPIRY_FORMAT)
+        return credentials
+
+    def create_scoped_required(self):
+        return not self._scopes
+
+    def create_scoped(self, scopes):
+        result = self.__class__(self._service_account_email,
+                                self._signer,
+                                scopes=scopes,
+                                private_key_id=self._private_key_id,
+                                client_id=self.client_id,
+                                user_agent=self._user_agent,
+                                **self._kwargs)
+        result.token_uri = self.token_uri
+        result.revoke_uri = self.revoke_uri
+        result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
+        result._private_key_pkcs12 = self._private_key_pkcs12
+        result._private_key_password = self._private_key_password
+        return result
+
+    def create_with_claims(self, claims):
+        """Create credentials that specify additional claims.
+
+        Args:
+            claims: dict, key-value pairs for claims.
+
+        Returns:
+            ServiceAccountCredentials, a copy of the current service account
+            credentials with updated claims to use when obtaining access
+            tokens.
+        """
+        new_kwargs = dict(self._kwargs)
+        new_kwargs.update(claims)
+        result = self.__class__(self._service_account_email,
+                                self._signer,
+                                scopes=self._scopes,
+                                private_key_id=self._private_key_id,
+                                client_id=self.client_id,
+                                user_agent=self._user_agent,
+                                **new_kwargs)
+        result.token_uri = self.token_uri
+        result.revoke_uri = self.revoke_uri
+        result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
+        result._private_key_pkcs12 = self._private_key_pkcs12
+        result._private_key_password = self._private_key_password
+        return result
+
+    def create_delegated(self, sub):
+        """Create credentials that act as domain-wide delegation of authority.
+
+        Use the ``sub`` parameter as the subject to delegate on behalf of
+        that user.
+
+        For example::
+
+          >>> account_sub = 'foo@email.com'
+          >>> delegate_creds = creds.create_delegated(account_sub)
+
+        Args:
+            sub: string, An email address that this service account will
+                 act on behalf of (via domain-wide delegation).
+
+        Returns:
+            ServiceAccountCredentials, a copy of the current service account
+            updated to act on behalf of ``sub``.
+        """
+        return self.create_with_claims({'sub': sub})
+
+
+def _datetime_to_secs(utc_time):
+    # TODO(issue 298): use time_delta.total_seconds()
+    # time_delta.total_seconds() not supported in Python 2.6
+    epoch = datetime.datetime(1970, 1, 1)
+    time_delta = utc_time - epoch
+    return time_delta.days * 86400 + time_delta.seconds
+
+
+class _JWTAccessCredentials(ServiceAccountCredentials):
+    """Self signed JWT credentials.
+
+    Makes an assertion to server using a self signed JWT from service account
+    credentials.  These credentials do NOT use OAuth 2.0 and instead
+    authenticate directly.
+    """
+    _MAX_TOKEN_LIFETIME_SECS = 3600
+    """Max lifetime of the token (one hour, in seconds)."""
+
+    def __init__(self,
+                 service_account_email,
+                 signer,
+                 scopes=None,
+                 private_key_id=None,
+                 client_id=None,
+                 user_agent=None,
+                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+                 additional_claims=None):
+        if additional_claims is None:
+            additional_claims = {}
+        super(_JWTAccessCredentials, self).__init__(
+            service_account_email,
+            signer,
+            private_key_id=private_key_id,
+            client_id=client_id,
+            user_agent=user_agent,
+            token_uri=token_uri,
+            revoke_uri=revoke_uri,
+            **additional_claims)
+
+    def authorize(self, http):
+        """Authorize an httplib2.Http instance with a JWT assertion.
+
+        Unless specified, the 'aud' of the assertion will be the base
+        uri of the request.
+
+        Args:
+            http: An instance of ``httplib2.Http`` or something that acts
+                  like it.
+        Returns:
+            A modified instance of http that was passed in.
+        Example::
+            h = httplib2.Http()
+            h = credentials.authorize(h)
+        """
+        transport.wrap_http_for_jwt_access(self, http)
+        return http
+
+    def get_access_token(self, http=None, additional_claims=None):
+        """Create a signed jwt.
+
+        Args:
+            http: unused
+            additional_claims: dict, additional claims to add to
+                the payload of the JWT.
+        Returns:
+            An AccessTokenInfo with the signed jwt
+        """
+        if additional_claims is None:
+            if self.access_token is None or self.access_token_expired:
+                self.refresh(None)
+            return client.AccessTokenInfo(
+              access_token=self.access_token, expires_in=self._expires_in())
+        else:
+            # Create a 1 time token
+            token, unused_expiry = self._create_token(additional_claims)
+            return client.AccessTokenInfo(
+              access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
+
+    def revoke(self, http):
+        """Cannot revoke JWTAccessCredentials tokens."""
+        pass
+
+    def create_scoped_required(self):
+        # JWTAccessCredentials are unscoped by definition
+        return True
+
+    def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
+                      revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+        # Returns an OAuth2 credentials with the given scope
+        result = ServiceAccountCredentials(self._service_account_email,
+                                           self._signer,
+                                           scopes=scopes,
+                                           private_key_id=self._private_key_id,
+                                           client_id=self.client_id,
+                                           user_agent=self._user_agent,
+                                           token_uri=token_uri,
+                                           revoke_uri=revoke_uri,
+                                           **self._kwargs)
+        if self._private_key_pkcs8_pem is not None:
+            result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
+        if self._private_key_pkcs12 is not None:
+            result._private_key_pkcs12 = self._private_key_pkcs12
+        if self._private_key_password is not None:
+            result._private_key_password = self._private_key_password
+        return result
+
+    def refresh(self, http):
+        self._refresh(None)
+
+    def _refresh(self, http_request):
+        self.access_token, self.token_expiry = self._create_token()
+
+    def _create_token(self, additional_claims=None):
+        now = client._UTCNOW()
+        lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS)
+        expiry = now + lifetime
+        payload = {
+            'iat': _datetime_to_secs(now),
+            'exp': _datetime_to_secs(expiry),
+            'iss': self._service_account_email,
+            'sub': self._service_account_email
+        }
+        payload.update(self._kwargs)
+        if additional_claims is not None:
+            payload.update(additional_claims)
+        jwt = crypt.make_signed_jwt(self._signer, payload,
+                                    key_id=self._private_key_id)
+        return jwt.decode('ascii'), expiry
diff --git a/oauth2client/tools.py b/oauth2client/tools.py
new file mode 100644
index 0000000..8947157
--- /dev/null
+++ b/oauth2client/tools.py
@@ -0,0 +1,256 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Command-line tools for authenticating via OAuth 2.0
+
+Do the OAuth 2.0 Web Server dance for a command line application. Stores the
+generated credentials in a common file that is used by other example apps in
+the same directory.
+"""
+
+from __future__ import print_function
+
+import logging
+import socket
+import sys
+
+from six.moves import BaseHTTPServer
+from six.moves import http_client
+from six.moves import input
+from six.moves import urllib
+
+from oauth2client import client
+from oauth2client import util
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+__all__ = ['argparser', 'run_flow', 'message_if_missing']
+
+_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
+
+To make this sample run you will need to populate the client_secrets.json file
+found at:
+
+   {file_path}
+
+with information from the APIs Console <https://code.google.com/apis/console>.
+
+"""
+
+_FAILED_START_MESSAGE = """
+Failed to start a local webserver listening on either port 8080
+or port 8090. Please check your firewall settings and locally
+running programs that may be blocking or using those ports.
+
+Falling back to --noauth_local_webserver and continuing with
+authorization.
+"""
+
+_BROWSER_OPENED_MESSAGE = """
+Your browser has been opened to visit:
+
+    {address}
+
+If your browser is on a different machine then exit and re-run this
+application with the command-line parameter
+
+  --noauth_local_webserver
+"""
+
+_GO_TO_LINK_MESSAGE = """
+Go to the following link in your browser:
+
+    {address}
+"""
+
+
+def _CreateArgumentParser():
+    try:
+        import argparse
+    except ImportError:  # pragma: NO COVER
+        return None
+    parser = argparse.ArgumentParser(add_help=False)
+    parser.add_argument('--auth_host_name', default='localhost',
+                        help='Hostname when running a local web server.')
+    parser.add_argument('--noauth_local_webserver', action='store_true',
+                        default=False, help='Do not run a local web server.')
+    parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
+                        nargs='*', help='Port web server should listen on.')
+    parser.add_argument(
+        '--logging_level', default='ERROR',
+        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
+        help='Set the logging level of detail.')
+    return parser
+
+# argparser is an ArgumentParser that contains command-line options expected
+# by tools.run(). Pass it in as part of the 'parents' argument to your own
+# ArgumentParser.
+argparser = _CreateArgumentParser()
+
+
+class ClientRedirectServer(BaseHTTPServer.HTTPServer):
+    """A server to handle OAuth 2.0 redirects back to localhost.
+
+    Waits for a single request and parses the query parameters
+    into query_params and then stops serving.
+    """
+    query_params = {}
+
+
+class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    """A handler for OAuth 2.0 redirects back to localhost.
+
+    Waits for a single request and parses the query parameters
+    into the servers query_params and then stops serving.
+    """
+
+    def do_GET(self):
+        """Handle a GET request.
+
+        Parses the query parameters and prints a message
+        if the flow has completed. Note that we can't detect
+        if an error occurred.
+        """
+        self.send_response(http_client.OK)
+        self.send_header("Content-type", "text/html")
+        self.end_headers()
+        query = self.path.split('?', 1)[-1]
+        query = dict(urllib.parse.parse_qsl(query))
+        self.server.query_params = query
+        self.wfile.write(
+            b"<html><head><title>Authentication Status</title></head>")
+        self.wfile.write(
+            b"<body><p>The authentication flow has completed.</p>")
+        self.wfile.write(b"</body></html>")
+
+    def log_message(self, format, *args):
+        """Do not log messages to stdout while running as cmd. line program."""
+
+
+@util.positional(3)
+def run_flow(flow, storage, flags=None, http=None):
+    """Core code for a command-line application.
+
+    The ``run()`` function is called from your application and runs
+    through all the steps to obtain credentials. It takes a ``Flow``
+    argument and attempts to open an authorization server page in the
+    user's default web browser. The server asks the user to grant your
+    application access to the user's data. If the user grants access,
+    the ``run()`` function returns new credentials. The new credentials
+    are also stored in the ``storage`` argument, which updates the file
+    associated with the ``Storage`` object.
+
+    It presumes it is run from a command-line application and supports the
+    following flags:
+
+        ``--auth_host_name`` (string, default: ``localhost``)
+           Host name to use when running a local web server to handle
+           redirects during OAuth authorization.
+
+        ``--auth_host_port`` (integer, default: ``[8080, 8090]``)
+           Port to use when running a local web server to handle redirects
+           during OAuth authorization. Repeat this option to specify a list
+           of values.
+
+        ``--[no]auth_local_webserver`` (boolean, default: ``True``)
+           Run a local web server to handle redirects during OAuth
+           authorization.
+
+    The tools module defines an ``ArgumentParser`` the already contains the
+    flag definitions that ``run()`` requires. You can pass that
+    ``ArgumentParser`` to your ``ArgumentParser`` constructor::
+
+        parser = argparse.ArgumentParser(
+            description=__doc__,
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            parents=[tools.argparser])
+        flags = parser.parse_args(argv)
+
+    Args:
+        flow: Flow, an OAuth 2.0 Flow to step through.
+        storage: Storage, a ``Storage`` to store the credential in.
+        flags: ``argparse.Namespace``, (Optional) The command-line flags. This
+               is the object returned from calling ``parse_args()`` on
+               ``argparse.ArgumentParser`` as described above. Defaults
+               to ``argparser.parse_args()``.
+        http: An instance of ``httplib2.Http.request`` or something that
+              acts like it.
+
+    Returns:
+        Credentials, the obtained credential.
+    """
+    if flags is None:
+        flags = argparser.parse_args()
+    logging.getLogger().setLevel(getattr(logging, flags.logging_level))
+    if not flags.noauth_local_webserver:
+        success = False
+        port_number = 0
+        for port in flags.auth_host_port:
+            port_number = port
+            try:
+                httpd = ClientRedirectServer((flags.auth_host_name, port),
+                                             ClientRedirectHandler)
+            except socket.error:
+                pass
+            else:
+                success = True
+                break
+        flags.noauth_local_webserver = not success
+        if not success:
+            print(_FAILED_START_MESSAGE)
+
+    if not flags.noauth_local_webserver:
+        oauth_callback = 'http://{host}:{port}/'.format(
+            host=flags.auth_host_name, port=port_number)
+    else:
+        oauth_callback = client.OOB_CALLBACK_URN
+    flow.redirect_uri = oauth_callback
+    authorize_url = flow.step1_get_authorize_url()
+
+    if not flags.noauth_local_webserver:
+        import webbrowser
+        webbrowser.open(authorize_url, new=1, autoraise=True)
+        print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url))
+    else:
+        print(_GO_TO_LINK_MESSAGE.format(address=authorize_url))
+
+    code = None
+    if not flags.noauth_local_webserver:
+        httpd.handle_request()
+        if 'error' in httpd.query_params:
+            sys.exit('Authentication request was rejected.')
+        if 'code' in httpd.query_params:
+            code = httpd.query_params['code']
+        else:
+            print('Failed to find "code" in the query parameters '
+                  'of the redirect.')
+            sys.exit('Try running with --noauth_local_webserver.')
+    else:
+        code = input('Enter verification code: ').strip()
+
+    try:
+        credential = flow.step2_exchange(code, http=http)
+    except client.FlowExchangeError as e:
+        sys.exit('Authentication has failed: {0}'.format(e))
+
+    storage.put(credential)
+    credential.set_store(storage)
+    print('Authentication successful.')
+
+    return credential
+
+
+def message_if_missing(filename):
+    """Helpful message to display if the CLIENT_SECRETS file is missing."""
+    return _CLIENT_SECRETS_MESSAGE.format(file_path=filename)
diff --git a/oauth2client/transport.py b/oauth2client/transport.py
new file mode 100644
index 0000000..8dbc60d
--- /dev/null
+++ b/oauth2client/transport.py
@@ -0,0 +1,245 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import logging
+
+import httplib2
+import six
+from six.moves import http_client
+
+from oauth2client._helpers import _to_bytes
+
+
+_LOGGER = logging.getLogger(__name__)
+# Properties present in file-like streams / buffers.
+_STREAM_PROPERTIES = ('read', 'seek', 'tell')
+
+# Google Data client libraries may need to set this to [401, 403].
+REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
+
+
+class MemoryCache(object):
+    """httplib2 Cache implementation which only caches locally."""
+
+    def __init__(self):
+        self.cache = {}
+
+    def get(self, key):
+        return self.cache.get(key)
+
+    def set(self, key, value):
+        self.cache[key] = value
+
+    def delete(self, key):
+        self.cache.pop(key, None)
+
+
+def get_cached_http():
+    """Return an HTTP object which caches results returned.
+
+    This is intended to be used in methods like
+    oauth2client.client.verify_id_token(), which calls to the same URI
+    to retrieve certs.
+
+    Returns:
+        httplib2.Http, an HTTP object with a MemoryCache
+    """
+    return _CACHED_HTTP
+
+
+def get_http_object():
+    """Return a new HTTP object.
+
+    Returns:
+        httplib2.Http, an HTTP object.
+    """
+    return httplib2.Http()
+
+
+def _initialize_headers(headers):
+    """Creates a copy of the headers.
+
+    Args:
+        headers: dict, request headers to copy.
+
+    Returns:
+        dict, the copied headers or a new dictionary if the headers
+        were None.
+    """
+    return {} if headers is None else dict(headers)
+
+
+def _apply_user_agent(headers, user_agent):
+    """Adds a user-agent to the headers.
+
+    Args:
+        headers: dict, request headers to add / modify user
+                 agent within.
+        user_agent: str, the user agent to add.
+
+    Returns:
+        dict, the original headers passed in, but modified if the
+        user agent is not None.
+    """
+    if user_agent is not None:
+        if 'user-agent' in headers:
+            headers['user-agent'] = (user_agent + ' ' + headers['user-agent'])
+        else:
+            headers['user-agent'] = user_agent
+
+    return headers
+
+
+def clean_headers(headers):
+    """Forces header keys and values to be strings, i.e not unicode.
+
+    The httplib module just concats the header keys and values in a way that
+    may make the message header a unicode string, which, if it then tries to
+    contatenate to a binary request body may result in a unicode decode error.
+
+    Args:
+        headers: dict, A dictionary of headers.
+
+    Returns:
+        The same dictionary but with all the keys converted to strings.
+    """
+    clean = {}
+    try:
+        for k, v in six.iteritems(headers):
+            if not isinstance(k, six.binary_type):
+                k = str(k)
+            if not isinstance(v, six.binary_type):
+                v = str(v)
+            clean[_to_bytes(k)] = _to_bytes(v)
+    except UnicodeEncodeError:
+        from oauth2client.client import NonAsciiHeaderError
+        raise NonAsciiHeaderError(k, ': ', v)
+    return clean
+
+
+def wrap_http_for_auth(credentials, http):
+    """Prepares an HTTP object's request method for auth.
+
+    Wraps HTTP requests with logic to catch auth failures (typically
+    identified via a 401 status code). In the event of failure, tries
+    to refresh the token used and then retry the original request.
+
+    Args:
+        credentials: Credentials, the credentials used to identify
+                     the authenticated user.
+        http: httplib2.Http, an http object to be used to make
+              auth requests.
+    """
+    orig_request_method = http.request
+
+    # The closure that will replace 'httplib2.Http.request'.
+    def new_request(uri, method='GET', body=None, headers=None,
+                    redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+                    connection_type=None):
+        if not credentials.access_token:
+            _LOGGER.info('Attempting refresh to obtain '
+                         'initial access_token')
+            credentials._refresh(orig_request_method)
+
+        # Clone and modify the request headers to add the appropriate
+        # Authorization header.
+        headers = _initialize_headers(headers)
+        credentials.apply(headers)
+        _apply_user_agent(headers, credentials.user_agent)
+
+        body_stream_position = None
+        # Check if the body is a file-like stream.
+        if all(getattr(body, stream_prop, None) for stream_prop in
+               _STREAM_PROPERTIES):
+            body_stream_position = body.tell()
+
+        resp, content = orig_request_method(uri, method, body,
+                                            clean_headers(headers),
+                                            redirections, connection_type)
+
+        # A stored token may expire between the time it is retrieved and
+        # the time the request is made, so we may need to try twice.
+        max_refresh_attempts = 2
+        for refresh_attempt in range(max_refresh_attempts):
+            if resp.status not in REFRESH_STATUS_CODES:
+                break
+            _LOGGER.info('Refreshing due to a %s (attempt %s/%s)',
+                         resp.status, refresh_attempt + 1,
+                         max_refresh_attempts)
+            credentials._refresh(orig_request_method)
+            credentials.apply(headers)
+            if body_stream_position is not None:
+                body.seek(body_stream_position)
+
+            resp, content = orig_request_method(uri, method, body,
+                                                clean_headers(headers),
+                                                redirections, connection_type)
+
+        return resp, content
+
+    # Replace the request method with our own closure.
+    http.request = new_request
+
+    # Set credentials as a property of the request method.
+    setattr(http.request, 'credentials', credentials)
+
+
+def wrap_http_for_jwt_access(credentials, http):
+    """Prepares an HTTP object's request method for JWT access.
+
+    Wraps HTTP requests with logic to catch auth failures (typically
+    identified via a 401 status code). In the event of failure, tries
+    to refresh the token used and then retry the original request.
+
+    Args:
+        credentials: _JWTAccessCredentials, the credentials used to identify
+                     a service account that uses JWT access tokens.
+        http: httplib2.Http, an http object to be used to make
+              auth requests.
+    """
+    orig_request_method = http.request
+    wrap_http_for_auth(credentials, http)
+    # The new value of ``http.request`` set by ``wrap_http_for_auth``.
+    authenticated_request_method = http.request
+
+    # The closure that will replace 'httplib2.Http.request'.
+    def new_request(uri, method='GET', body=None, headers=None,
+                    redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+                    connection_type=None):
+        if 'aud' in credentials._kwargs:
+            # Preemptively refresh token, this is not done for OAuth2
+            if (credentials.access_token is None or
+                    credentials.access_token_expired):
+                credentials.refresh(None)
+            return authenticated_request_method(uri, method, body,
+                                                headers, redirections,
+                                                connection_type)
+        else:
+            # If we don't have an 'aud' (audience) claim,
+            # create a 1-time token with the uri root as the audience
+            headers = _initialize_headers(headers)
+            _apply_user_agent(headers, credentials.user_agent)
+            uri_root = uri.split('?', 1)[0]
+            token, unused_expiry = credentials._create_token({'aud': uri_root})
+
+            headers['Authorization'] = 'Bearer ' + token
+            return orig_request_method(uri, method, body,
+                                       clean_headers(headers),
+                                       redirections, connection_type)
+
+    # Replace the request method with our own closure.
+    http.request = new_request
+
+
+_CACHED_HTTP = httplib2.Http(MemoryCache())
diff --git a/oauth2client/util.py b/oauth2client/util.py
new file mode 100644
index 0000000..e3ba62b
--- /dev/null
+++ b/oauth2client/util.py
@@ -0,0 +1,206 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Common utility library."""
+
+import functools
+import inspect
+import logging
+
+import six
+from six.moves import urllib
+
+
+__author__ = [
+    'rafek@google.com (Rafe Kaplan)',
+    'guido@google.com (Guido van Rossum)',
+]
+
+__all__ = [
+    'positional',
+    'POSITIONAL_WARNING',
+    'POSITIONAL_EXCEPTION',
+    'POSITIONAL_IGNORE',
+]
+
+logger = logging.getLogger(__name__)
+
+POSITIONAL_WARNING = 'WARNING'
+POSITIONAL_EXCEPTION = 'EXCEPTION'
+POSITIONAL_IGNORE = 'IGNORE'
+POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
+                            POSITIONAL_IGNORE])
+
+positional_parameters_enforcement = POSITIONAL_WARNING
+
+
+def positional(max_positional_args):
+    """A decorator to declare that only the first N arguments my be positional.
+
+    This decorator makes it easy to support Python 3 style keyword-only
+    parameters. For example, in Python 3 it is possible to write::
+
+        def fn(pos1, *, kwonly1=None, kwonly1=None):
+            ...
+
+    All named parameters after ``*`` must be a keyword::
+
+        fn(10, 'kw1', 'kw2')  # Raises exception.
+        fn(10, kwonly1='kw1')  # Ok.
+
+    Example
+    ^^^^^^^
+
+    To define a function like above, do::
+
+        @positional(1)
+        def fn(pos1, kwonly1=None, kwonly2=None):
+            ...
+
+    If no default value is provided to a keyword argument, it becomes a
+    required keyword argument::
+
+        @positional(0)
+        def fn(required_kw):
+            ...
+
+    This must be called with the keyword parameter::
+
+        fn()  # Raises exception.
+        fn(10)  # Raises exception.
+        fn(required_kw=10)  # Ok.
+
+    When defining instance or class methods always remember to account for
+    ``self`` and ``cls``::
+
+        class MyClass(object):
+
+            @positional(2)
+            def my_method(self, pos1, kwonly1=None):
+                ...
+
+            @classmethod
+            @positional(2)
+            def my_method(cls, pos1, kwonly1=None):
+                ...
+
+    The positional decorator behavior is controlled by
+    ``util.positional_parameters_enforcement``, which may be set to
+    ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
+    ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
+    nothing, respectively, if a declaration is violated.
+
+    Args:
+        max_positional_arguments: Maximum number of positional arguments. All
+                                  parameters after the this index must be
+                                  keyword only.
+
+    Returns:
+        A decorator that prevents using arguments after max_positional_args
+        from being used as positional parameters.
+
+    Raises:
+        TypeError: if a key-word only argument is provided as a positional
+                   parameter, but only if
+                   util.positional_parameters_enforcement is set to
+                   POSITIONAL_EXCEPTION.
+    """
+
+    def positional_decorator(wrapped):
+        @functools.wraps(wrapped)
+        def positional_wrapper(*args, **kwargs):
+            if len(args) > max_positional_args:
+                plural_s = ''
+                if max_positional_args != 1:
+                    plural_s = 's'
+                message = ('{function}() takes at most {args_max} positional '
+                           'argument{plural} ({args_given} given)'.format(
+                               function=wrapped.__name__,
+                               args_max=max_positional_args,
+                               args_given=len(args),
+                               plural=plural_s))
+                if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
+                    raise TypeError(message)
+                elif positional_parameters_enforcement == POSITIONAL_WARNING:
+                    logger.warning(message)
+            return wrapped(*args, **kwargs)
+        return positional_wrapper
+
+    if isinstance(max_positional_args, six.integer_types):
+        return positional_decorator
+    else:
+        args, _, _, defaults = inspect.getargspec(max_positional_args)
+        return positional(len(args) - len(defaults))(max_positional_args)
+
+
+def scopes_to_string(scopes):
+    """Converts scope value to a string.
+
+    If scopes is a string then it is simply passed through. If scopes is an
+    iterable then a string is returned that is all the individual scopes
+    concatenated with spaces.
+
+    Args:
+        scopes: string or iterable of strings, the scopes.
+
+    Returns:
+        The scopes formatted as a single string.
+    """
+    if isinstance(scopes, six.string_types):
+        return scopes
+    else:
+        return ' '.join(scopes)
+
+
+def string_to_scopes(scopes):
+    """Converts stringifed scope value to a list.
+
+    If scopes is a list then it is simply passed through. If scopes is an
+    string then a list of each individual scope is returned.
+
+    Args:
+        scopes: a string or iterable of strings, the scopes.
+
+    Returns:
+        The scopes in a list.
+    """
+    if not scopes:
+        return []
+    if isinstance(scopes, six.string_types):
+        return scopes.split(' ')
+    else:
+        return scopes
+
+
+def _add_query_parameter(url, name, value):
+    """Adds a query parameter to a url.
+
+    Replaces the current value if it already exists in the URL.
+
+    Args:
+        url: string, url to add the query parameter to.
+        name: string, query parameter name.
+        value: string, query parameter value.
+
+    Returns:
+        Updated query parameter. Does not update the url if value is None.
+    """
+    if value is None:
+        return url
+    else:
+        parsed = list(urllib.parse.urlparse(url))
+        q = dict(urllib.parse.parse_qsl(parsed[4]))
+        q[name] = value
+        parsed[4] = urllib.parse.urlencode(q)
+        return urllib.parse.urlunparse(parsed)
diff --git a/samples/call_compute_service.py b/samples/call_compute_service.py
new file mode 100644
index 0000000..72beef0
--- /dev/null
+++ b/samples/call_compute_service.py
@@ -0,0 +1,22 @@
+# To be used to test GoogleCredentials.get_application_default()
+# from local machine and GCE.
+# The GCE virtual machine needs to have both service account and
+# Compute API enabled.
+# See:  https://developers.google.com/compute/docs/authentication
+
+from googleapiclient.discovery import build
+
+from oauth2client.client import GoogleCredentials
+
+
+PROJECT = 'bamboo-machine-422'  # Provide your own GCE project here
+ZONE = 'us-central1-a'          # Put here a zone which has some VMs
+
+
+credentials = GoogleCredentials.get_application_default()
+service = build('compute', 'v1', credentials=credentials)
+
+request = service.instances().list(project=PROJECT, zone=ZONE)
+response = request.execute()
+
+print(response)
diff --git a/samples/googleappengine/app.yaml b/samples/googleappengine/app.yaml
new file mode 100644
index 0000000..a299030
--- /dev/null
+++ b/samples/googleappengine/app.yaml
@@ -0,0 +1,10 @@
+application: bamboo-machine-422
+version: 2
+runtime: python27
+api_version: 1
+threadsafe: true
+
+handlers:
+- url: /.*
+  script: call_compute_service_from_gae.app
+
diff --git a/samples/googleappengine/call_compute_service_from_gae.py b/samples/googleappengine/call_compute_service_from_gae.py
new file mode 100644
index 0000000..3557f58
--- /dev/null
+++ b/samples/googleappengine/call_compute_service_from_gae.py
@@ -0,0 +1,27 @@
+# To be used to test GoogleCredentials.get_application_default()
+# from devel GAE (ie, dev_appserver.py).
+
+from googleapiclient.discovery import build
+import webapp2
+
+from oauth2client.client import GoogleCredentials
+
+
+PROJECT = 'bamboo-machine-422'  # Provide your own GCE project here
+ZONE = 'us-central1-a'          # Put here a zone which has some VMs
+
+
+def get_instances():
+    credentials = GoogleCredentials.get_application_default()
+    service = build('compute', 'v1', credentials=credentials)
+    request = service.instances().list(project=PROJECT, zone=ZONE)
+    return request.execute()
+
+
+class MainPage(webapp2.RequestHandler):
+
+    def get(self):
+        self.response.write(get_instances())
+
+
+app = webapp2.WSGIApplication([('/', MainPage), ], debug=True)
diff --git a/samples/oauth2_for_devices.py b/samples/oauth2_for_devices.py
new file mode 100644
index 0000000..6e70ff1
--- /dev/null
+++ b/samples/oauth2_for_devices.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+
+# See:  https://developers.google.com/accounts/docs/OAuth2ForDevices
+
+from googleapiclient.discovery import build
+import httplib2
+from six.moves import input
+
+from oauth2client.client import OAuth2WebServerFlow
+
+CLIENT_ID = "some+client+id"
+CLIENT_SECRET = "some+client+secret"
+SCOPES = ("https://www.googleapis.com/auth/youtube",)
+
+flow = OAuth2WebServerFlow(CLIENT_ID, CLIENT_SECRET, " ".join(SCOPES))
+
+# Step 1: get user code and verification URL
+# https://developers.google.com/accounts/docs/OAuth2ForDevices#obtainingacode
+flow_info = flow.step1_get_device_and_user_codes()
+print("Enter the following code at {0}: {1}".format(flow_info.verification_url,
+                                                    flow_info.user_code))
+print("Then press Enter.")
+input()
+
+# Step 2: get credentials
+# https://developers.google.com/accounts/docs/OAuth2ForDevices#obtainingatoken
+credentials = flow.step2_exchange(device_flow_info=flow_info)
+print("Access token:  {0}".format(credentials.access_token))
+print("Refresh token: {0}".format(credentials.refresh_token))
+
+# Get YouTube service
+# https://developers.google.com/accounts/docs/OAuth2ForDevices#callinganapi
+youtube = build("youtube", "v3", http=credentials.authorize(httplib2.Http()))
diff --git a/scripts/build_docs.sh b/scripts/build_docs.sh
new file mode 100755
index 0000000..170b113
--- /dev/null
+++ b/scripts/build_docs.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+#
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+#
+# Build the oauth2client docs.
+
+set -e
+
+rm -rf docs/_build/* docs/source/*
+sphinx-apidoc --separate --force -o docs/source oauth2client
+# We only have one package, so modules.rst is overkill.
+rm -f docs/source/modules.rst
+
+# If anything has changed
+if [[ -n "$(git diff -- docs/)" ]]; then
+    echo "sphinx-apidoc generated changes that are not checked in to version control."
+    exit 1
+fi
+
+cd docs
+make html
+cd ..
diff --git a/scripts/fetch_gae_sdk.py b/scripts/fetch_gae_sdk.py
new file mode 100755
index 0000000..24a6db5
--- /dev/null
+++ b/scripts/fetch_gae_sdk.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+"""Fetch the most recent GAE SDK and decompress it in the current directory.
+
+Usage:
+    fetch_gae_sdk.py [<dest_dir>]
+
+Current releases are listed here:
+    https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured
+"""
+from __future__ import print_function
+
+import json
+import os
+import StringIO
+import sys
+import urllib2
+import zipfile
+
+
+_SDK_URL = (
+    'https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured')
+
+
+def get_gae_versions():
+    try:
+        version_info_json = urllib2.urlopen(_SDK_URL).read()
+    except:
+        return {}
+    try:
+        version_info = json.loads(version_info_json)
+    except:
+        return {}
+    return version_info.get('items', {})
+
+
+def _version_tuple(v):
+    version_string = os.path.splitext(v['name'])[0].rpartition('_')[2]
+    return tuple(int(x) for x in version_string.split('.'))
+
+
+def get_sdk_urls(sdk_versions):
+    python_releases = [v for v in sdk_versions
+                       if v['name'].startswith('featured/google_appengine')]
+    current_releases = sorted(python_releases, key=_version_tuple,
+                              reverse=True)
+    return [release['mediaLink'] for release in current_releases]
+
+
+def main(argv):
+    if len(argv) > 2:
+        print('Usage: {0} [<destination_dir>]'.format(argv[0]))
+        return 1
+    dest_dir = argv[1] if len(argv) > 1 else '.'
+    if not os.path.exists(dest_dir):
+        os.makedirs(dest_dir)
+
+    if os.path.exists(os.path.join(dest_dir, 'google_appengine')):
+        print('GAE SDK already installed at {0}, exiting.'.format(dest_dir))
+        return 0
+
+    sdk_versions = get_gae_versions()
+    if not sdk_versions:
+        print('Error fetching GAE SDK version info')
+        return 1
+    sdk_urls = get_sdk_urls(sdk_versions)
+    for sdk_url in sdk_urls:
+        try:
+            sdk_contents = StringIO.StringIO(urllib2.urlopen(sdk_url).read())
+            break
+        except:
+            pass
+    else:
+        print('Could not read SDK from any of ', sdk_urls)
+        return 1
+    sdk_contents.seek(0)
+    try:
+        zip_contents = zipfile.ZipFile(sdk_contents)
+        zip_contents.extractall(dest_dir)
+    except:
+        print('Error extracting SDK contents')
+        return 1
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[:]))
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100755
index 0000000..0ef7ad2
--- /dev/null
+++ b/scripts/install.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+set -ev
+
+pip install tox
+if [[ "${TOX_ENV}" == "pypy" ]]; then
+    git clone https://github.com/yyuu/pyenv.git ${HOME}/.pyenv
+    PYENV_ROOT="${HOME}/.pyenv"
+    PATH="${PYENV_ROOT}/bin:${PATH}"
+    eval "$(pyenv init -)"
+    pyenv install pypy-2.6.0
+    pyenv global pypy-2.6.0
+fi
+
+if [[ "${TOX_ENV}" == "gae" && ! -d ${GAE_PYTHONPATH} ]]; then
+    python scripts/fetch_gae_sdk.py `dirname ${GAE_PYTHONPATH}`
+fi
diff --git a/scripts/local_test_setup.sample b/scripts/local_test_setup.sample
new file mode 100644
index 0000000..a7a3d10
--- /dev/null
+++ b/scripts/local_test_setup.sample
@@ -0,0 +1,5 @@
+export OAUTH2CLIENT_TEST_JSON_KEY_PATH="tests/data/gcloud/application_default_credentials.json"
+export OAUTH2CLIENT_TEST_P12_KEY_PATH="tests/data/privatekey.p12"
+export OAUTH2CLIENT_TEST_P12_KEY_EMAIL="project-foo@developer.gserviceaccount.com"
+export OAUTH2CLIENT_TEST_USER_KEY_PATH="tests/data/gcloud/application_default_credentials_authorized_user.json"
+export OAUTH2CLIENT_TEST_USER_KEY_EMAIL="foo@gmail.com"
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100755
index 0000000..0b537e2
--- /dev/null
+++ b/scripts/run.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+set -ev
+
+if [[ "${TOX_ENV}" == "pypy" ]]; then
+    PYENV_ROOT="${HOME}/.pyenv"
+    PATH="${PYENV_ROOT}/bin:${PATH}"
+    eval "$(pyenv init -)"
+    pyenv global pypy-2.6.0
+fi
+tox -e ${TOX_ENV}
diff --git a/scripts/run_gce_system_tests.py b/scripts/run_gce_system_tests.py
new file mode 100644
index 0000000..d446f9c
--- /dev/null
+++ b/scripts/run_gce_system_tests.py
@@ -0,0 +1,56 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import json
+
+import httplib2
+from six.moves import http_client
+from six.moves import urllib
+import unittest2
+
+from oauth2client import GOOGLE_TOKEN_INFO_URI
+from oauth2client.client import GoogleCredentials
+from oauth2client.contrib.gce import AppAssertionCredentials
+
+
+class TestComputeEngine(unittest2.TestCase):
+
+    def test_application_default(self):
+        default_creds = GoogleCredentials.get_application_default()
+        self.assertIsInstance(default_creds, AppAssertionCredentials)
+
+    def test_token_info(self):
+        credentials = AppAssertionCredentials([])
+        http = httplib2.Http()
+
+        # First refresh to get the access token.
+        self.assertIsNone(credentials.access_token)
+        credentials.refresh(http)
+        self.assertIsNotNone(credentials.access_token)
+
+        # Then check the access token against the token info API.
+        query_params = {'access_token': credentials.access_token}
+        token_uri = (GOOGLE_TOKEN_INFO_URI + '?' +
+                     urllib.parse.urlencode(query_params))
+        response, content = http.request(token_uri)
+        self.assertEqual(response.status, http_client.OK)
+
+        content = content.decode('utf-8')
+        payload = json.loads(content)
+        self.assertEqual(payload['access_type'], 'offline')
+        self.assertLessEqual(int(payload['expires_in']), 3600)
+
+
+if __name__ == '__main__':
+    unittest2.main()
diff --git a/scripts/run_system_tests.py b/scripts/run_system_tests.py
new file mode 100644
index 0000000..ce99e7c
--- /dev/null
+++ b/scripts/run_system_tests.py
@@ -0,0 +1,108 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import json
+import os
+
+import httplib2
+from six.moves import http_client
+
+import oauth2client
+from oauth2client import client
+from oauth2client.service_account import ServiceAccountCredentials
+
+
+JSON_KEY_PATH = os.getenv('OAUTH2CLIENT_TEST_JSON_KEY_PATH')
+P12_KEY_PATH = os.getenv('OAUTH2CLIENT_TEST_P12_KEY_PATH')
+P12_KEY_EMAIL = os.getenv('OAUTH2CLIENT_TEST_P12_KEY_EMAIL')
+USER_KEY_PATH = os.getenv('OAUTH2CLIENT_TEST_USER_KEY_PATH')
+USER_KEY_EMAIL = os.getenv('OAUTH2CLIENT_TEST_USER_KEY_EMAIL')
+
+SCOPE = ('https://www.googleapis.com/auth/plus.login',
+         'https://www.googleapis.com/auth/plus.me',
+         'https://www.googleapis.com/auth/userinfo.email',
+         'https://www.googleapis.com/auth/userinfo.profile')
+USER_INFO = 'https://www.googleapis.com/oauth2/v2/userinfo'
+
+
+def _require_environ():
+    if (JSON_KEY_PATH is None or P12_KEY_PATH is None or
+            P12_KEY_EMAIL is None or USER_KEY_PATH is None or
+            USER_KEY_EMAIL is None):
+        raise EnvironmentError('Expected environment variables to be set:',
+                               'OAUTH2CLIENT_TEST_JSON_KEY_PATH',
+                               'OAUTH2CLIENT_TEST_P12_KEY_PATH',
+                               'OAUTH2CLIENT_TEST_P12_KEY_EMAIL',
+                               'OAUTH2CLIENT_TEST_USER_KEY_PATH',
+                               'OAUTH2CLIENT_TEST_USER_KEY_EMAIL')
+
+    if not os.path.isfile(JSON_KEY_PATH):
+        raise EnvironmentError(JSON_KEY_PATH, 'is not a file')
+    if not os.path.isfile(P12_KEY_PATH):
+        raise EnvironmentError(P12_KEY_PATH, 'is not a file')
+    if not os.path.isfile(USER_KEY_PATH):
+        raise EnvironmentError(USER_KEY_PATH, 'is not a file')
+
+
+def _check_user_info(credentials, expected_email):
+    http = credentials.authorize(httplib2.Http())
+    response, content = http.request(USER_INFO)
+    if response.status != http_client.OK:
+        raise ValueError('Expected 200 OK response.')
+
+    content = content.decode('utf-8')
+    payload = json.loads(content)
+    if payload['email'] != expected_email:
+        raise ValueError('User info email does not match credentials.')
+
+
+def run_json():
+    credentials = ServiceAccountCredentials.from_json_keyfile_name(
+        JSON_KEY_PATH, scopes=SCOPE)
+    service_account_email = credentials._service_account_email
+    _check_user_info(credentials, service_account_email)
+
+
+def run_p12():
+    credentials = ServiceAccountCredentials.from_p12_keyfile(
+        P12_KEY_EMAIL, P12_KEY_PATH, scopes=SCOPE)
+    _check_user_info(credentials, P12_KEY_EMAIL)
+
+
+def run_user_json():
+    with open(USER_KEY_PATH, 'r') as file_object:
+        client_credentials = json.load(file_object)
+
+    credentials = client.GoogleCredentials(
+        access_token=None,
+        client_id=client_credentials['client_id'],
+        client_secret=client_credentials['client_secret'],
+        refresh_token=client_credentials['refresh_token'],
+        token_expiry=None,
+        token_uri=oauth2client.GOOGLE_TOKEN_URI,
+        user_agent='Python client library',
+    )
+
+    _check_user_info(credentials, USER_KEY_EMAIL)
+
+
+def main():
+    _require_environ()
+    run_json()
+    run_p12()
+    run_user_json()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/run_system_tests.sh b/scripts/run_system_tests.sh
new file mode 100755
index 0000000..7169eb7
--- /dev/null
+++ b/scripts/run_system_tests.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+set -ev
+
+
+# If we're on Travis, we need to set up the environment.
+if [[ "${TRAVIS}" == "true" ]]; then
+  # If merging to master and not a pull request, run system test.
+  if [[ "${TRAVIS_BRANCH}" == "master" ]] && \
+         [[ "${TRAVIS_PULL_REQUEST}" == "false" ]]; then
+    echo "Running in Travis during merge, decrypting stored key file."
+    # Convert encrypted JSON key file into decrypted file to be used.
+    openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \
+        -iv ${OAUTH2CLIENT_IV} \
+        -in tests/data/key.json.enc \
+        -out ${OAUTH2CLIENT_TEST_JSON_KEY_PATH} -d
+    # Convert encrypted P12 key file into decrypted file to be used.
+    openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \
+        -iv ${OAUTH2CLIENT_IV} \
+        -in tests/data/key.p12.enc \
+        -out ${OAUTH2CLIENT_TEST_P12_KEY_PATH} -d
+    # Convert encrypted User JSON key file into decrypted file to be used.
+    openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \
+        -iv ${OAUTH2CLIENT_IV} \
+        -in tests/data/user-key.json.enc \
+        -out ${OAUTH2CLIENT_TEST_USER_KEY_PATH} -d
+  else
+    echo "Running in Travis during non-merge to master, doing nothing."
+    exit
+  fi
+fi
+
+# Run the system tests for each tested package.
+python scripts/run_system_tests.py
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..686d1db
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,72 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Setup script for oauth2client.
+
+Also installs included versions of third party libraries, if those libraries
+are not already installed.
+"""
+from __future__ import print_function
+
+import sys
+
+from setuptools import find_packages
+from setuptools import setup
+
+import oauth2client
+
+if sys.version_info < (2, 6):
+    print('oauth2client requires python2 version >= 2.6.', file=sys.stderr)
+    sys.exit(1)
+if (3, 1) <= sys.version_info < (3, 3):
+    print('oauth2client requires python3 version >= 3.3.', file=sys.stderr)
+    sys.exit(1)
+
+install_requires = [
+    'httplib2>=0.9.1',
+    'pyasn1>=0.1.7',
+    'pyasn1-modules>=0.0.5',
+    'rsa>=3.1.4',
+    'six>=1.6.1',
+]
+
+long_desc = """The oauth2client is a client library for OAuth 2.0."""
+
+version = oauth2client.__version__
+
+setup(
+    name="oauth2client",
+    version=version,
+    description="OAuth 2.0 client library",
+    long_description=long_desc,
+    author="Google Inc.",
+    url="http://github.com/google/oauth2client/",
+    install_requires=install_requires,
+    packages=find_packages(),
+    license="Apache 2.0",
+    keywords="google oauth 2.0 http client",
+    classifiers=[
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Development Status :: 5 - Production/Stable',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Apache Software License',
+        'Operating System :: POSIX',
+        'Topic :: Internet :: WWW/HTTP',
+    ],
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..5f6567c
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,22 @@
+# 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.
+
+"""Test package set-up."""
+
+from oauth2client import util
+
+__author__ = 'afshar@google.com (Ali Afshar)'
+
+
+def setup_package():
+    """Run on testing package."""
+    util.positional_parameters_enforcement = util.POSITIONAL_EXCEPTION
diff --git a/tests/contrib/__init__.py b/tests/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/contrib/__init__.py
diff --git a/tests/contrib/django_util/__init__.py b/tests/contrib/django_util/__init__.py
new file mode 100644
index 0000000..378c6ff
--- /dev/null
+++ b/tests/contrib/django_util/__init__.py
@@ -0,0 +1,44 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Setups the Django test environment and provides helper classes."""
+
+import django
+from django import test
+from django.contrib.sessions.backends.file import SessionStore
+from django.test.runner import DiscoverRunner
+
+django.setup()
+default_app_config = 'tests.contrib.django_util.apps.AppConfig'
+
+
+class TestWithDjangoEnvironment(test.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        django.setup()
+        cls.runner = DiscoverRunner()
+        cls.runner.setup_test_environment()
+        cls.old_config = cls.runner.setup_databases()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.runner.teardown_databases(cls.old_config)
+        cls.runner.teardown_test_environment()
+
+    def setUp(self):
+        self.factory = test.RequestFactory()
+
+        store = SessionStore()
+        store.save()
+        self.session = store
diff --git a/tests/contrib/django_util/apps.py b/tests/contrib/django_util/apps.py
new file mode 100644
index 0000000..da74dd5
--- /dev/null
+++ b/tests/contrib/django_util/apps.py
@@ -0,0 +1,27 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Defines a configuration for our test application.
+
+Having a test application enables us to use the Django test database and
+other useful features."""
+
+from django.apps import AppConfig
+
+
+class DjangoOrmTestApp(AppConfig):
+    """App Config for Django Helper."""
+    name = 'tests.contrib.django_util'
+    verbose_name = "Django Test App"
+    label = "DjangoORMTestApp"
diff --git a/tests/contrib/django_util/models.py b/tests/contrib/django_util/models.py
new file mode 100644
index 0000000..50bf68b
--- /dev/null
+++ b/tests/contrib/django_util/models.py
@@ -0,0 +1,25 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Models used in our tests"""
+
+from django.contrib.auth.models import User
+from django.db import models
+
+from oauth2client.contrib.django_util.models import CredentialsField
+
+
+class CredentialsModel(models.Model):
+    user_id = models.OneToOneField(User)
+    credentials = CredentialsField()
diff --git a/tests/contrib/django_util/settings.py b/tests/contrib/django_util/settings.py
new file mode 100644
index 0000000..0c78634
--- /dev/null
+++ b/tests/contrib/django_util/settings.py
@@ -0,0 +1,47 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Provides a base Django settings module used by the rest of the tests."""
+
+import os
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'oauth2client.contrib.django_util',
+    'tests.contrib.django_util.apps.DjangoOrmTestApp',
+]
+
+SECRET_KEY = 'this string is not a real django secret key'
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join('.', 'db.sqlite3'),
+    }
+}
+MIDDLEWARE_CLASSES = (
+    'django.contrib.sessions.middleware.SessionMiddleware'
+)
+
+ALLOWED_HOSTS = ['testserver']
+
+GOOGLE_OAUTH2_CLIENT_ID = 'client_id2'
+GOOGLE_OAUTH2_CLIENT_SECRET = 'hunter2'
+GOOGLE_OAUTH2_SCOPES = ('https://www.googleapis.com/auth/cloud-platform',)
+
+ROOT_URLCONF = 'tests.contrib.django_util.test_django_util'
diff --git a/tests/contrib/django_util/test_decorators.py b/tests/contrib/django_util/test_decorators.py
new file mode 100644
index 0000000..846c6dd
--- /dev/null
+++ b/tests/contrib/django_util/test_decorators.py
@@ -0,0 +1,246 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Tests for the django_util decorators."""
+
+import copy
+
+from django import http
+import django.conf
+from django.contrib.auth.models import AnonymousUser, User
+import mock
+from six.moves import http_client
+from six.moves import reload_module
+from six.moves.urllib import parse
+from tests.contrib.django_util import TestWithDjangoEnvironment
+
+import oauth2client.contrib.django_util
+from oauth2client.contrib.django_util import decorators
+
+
+class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(OAuth2EnabledDecoratorTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+
+        # OAuth2 Settings gets configured based on Django settings
+        # at import time, so in order for us to reload the settings
+        # we need to reload the module
+        reload_module(oauth2client.contrib.django_util)
+        self.user = User.objects.create_user(
+            username='bill', email='bill@example.com', password='hunter2')
+
+    def tearDown(self):
+        super(OAuth2EnabledDecoratorTest, self).tearDown()
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_no_credentials_without_credentials(self):
+        request = self.factory.get('/test')
+        request.session = self.session
+
+        @decorators.oauth_enabled
+        def test_view(request):
+            return http.HttpResponse("test")  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertEqual(response.status_code, http_client.OK)
+        self.assertIsNotNone(request.oauth)
+        self.assertFalse(request.oauth.has_credentials())
+        self.assertIsNone(request.oauth.http)
+
+    @mock.patch('oauth2client.client.OAuth2Credentials')
+    def test_has_credentials_in_storage(self, OAuth2Credentials):
+        request = self.factory.get('/test')
+        request.session = mock.MagicMock()
+
+        credentials_mock = mock.Mock(
+            scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
+        credentials_mock.has_scopes.return_value = True
+        credentials_mock.invalid = False
+        credentials_mock.scopes = set([])
+        OAuth2Credentials.from_json.return_value = credentials_mock
+
+        @decorators.oauth_enabled
+        def test_view(request):
+            return http.HttpResponse('test')
+
+        response = test_view(request)
+        self.assertEqual(response.status_code, http_client.OK)
+        self.assertEqual(response.content, b'test')
+        self.assertTrue(request.oauth.has_credentials())
+        self.assertIsNotNone(request.oauth.http)
+        self.assertSetEqual(
+            request.oauth.scopes,
+            set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
+
+    @mock.patch('oauth2client.contrib.dictionary_storage.DictionaryStorage')
+    def test_specified_scopes(self, dictionary_storage_mock):
+        request = self.factory.get('/test')
+        request.session = mock.MagicMock()
+
+        credentials_mock = mock.Mock(
+            scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
+        credentials_mock.has_scopes = True
+        credentials_mock.is_valid = True
+        dictionary_storage_mock.get.return_value = credentials_mock
+
+        @decorators.oauth_enabled(scopes=['additional-scope'])
+        def test_view(request):
+            return http.HttpResponse('hello world')  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertEqual(response.status_code, http_client.OK)
+        self.assertIsNotNone(request.oauth)
+        self.assertFalse(request.oauth.has_credentials())
+
+
+class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(OAuth2RequiredDecoratorTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+
+        reload_module(oauth2client.contrib.django_util)
+        self.user = User.objects.create_user(
+            username='bill', email='bill@example.com', password='hunter2')
+
+    def tearDown(self):
+        super(OAuth2RequiredDecoratorTest, self).tearDown()
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_redirects_without_credentials(self):
+        request = self.factory.get('/test')
+        request.session = self.session
+
+        @decorators.oauth_required
+        def test_view(request):
+            return http.HttpResponse('test')  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+        self.assertEqual(parse.urlparse(response['Location']).path,
+                         '/oauth2/oauth2authorize/')
+        self.assertIn(
+            'return_url=%2Ftest', parse.urlparse(response['Location']).query)
+
+        self.assertEqual(response.status_code,
+                         http.HttpResponseRedirect.status_code)
+
+    @mock.patch('oauth2client.contrib.django_util.UserOAuth2', autospec=True)
+    def test_has_credentials_in_storage(self, UserOAuth2):
+        request = self.factory.get('/test')
+        request.session = mock.MagicMock()
+
+        @decorators.oauth_required
+        def test_view(request):
+            return http.HttpResponse("test")
+
+        my_user_oauth = mock.MagicMock()
+
+        UserOAuth2.return_value = my_user_oauth
+        my_user_oauth.has_credentials.return_value = True
+
+        response = test_view(request)
+        self.assertEqual(response.status_code, http_client.OK)
+        self.assertEqual(response.content, b"test")
+
+    @mock.patch('oauth2client.client.OAuth2Credentials')
+    def test_has_credentials_in_storage_no_scopes(
+            self, OAuth2Credentials):
+        request = self.factory.get('/test')
+
+        request.session = mock.MagicMock()
+        credentials_mock = mock.Mock(
+            scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
+        credentials_mock.has_scopes.return_value = False
+
+        OAuth2Credentials.from_json.return_value = credentials_mock
+
+        @decorators.oauth_required
+        def test_view(request):
+            return http.HttpResponse("test")  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertEqual(
+            response.status_code, django.http.HttpResponseRedirect.status_code)
+
+    @mock.patch('oauth2client.client.OAuth2Credentials')
+    def test_specified_scopes(self, OAuth2Credentials):
+        request = self.factory.get('/test')
+        request.session = mock.MagicMock()
+
+        credentials_mock = mock.Mock(
+            scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
+        credentials_mock.has_scopes = False
+        OAuth2Credentials.from_json.return_value = credentials_mock
+
+        @decorators.oauth_required(scopes=['additional-scope'])
+        def test_view(request):
+            return http.HttpResponse("hello world")  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertEqual(
+            response.status_code, django.http.HttpResponseRedirect.status_code)
+
+
+class OAuth2RequiredDecoratorStorageModelTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(OAuth2RequiredDecoratorStorageModelTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+
+        STORAGE_MODEL = {
+            'model': 'tests.contrib.django_util.models.CredentialsModel',
+            'user_property': 'user_id',
+            'credentials_property': 'credentials'
+        }
+        django.conf.settings.GOOGLE_OAUTH2_STORAGE_MODEL = STORAGE_MODEL
+
+        reload_module(oauth2client.contrib.django_util)
+        self.user = User.objects.create_user(
+            username='bill', email='bill@example.com', password='hunter2')
+
+    def tearDown(self):
+        super(OAuth2RequiredDecoratorStorageModelTest, self).tearDown()
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_redirects_anonymous_to_login(self):
+        request = self.factory.get('/test')
+        request.session = self.session
+        request.user = AnonymousUser()
+
+        @decorators.oauth_required
+        def test_view(request):
+            return http.HttpResponse("test")  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+        self.assertEqual(parse.urlparse(response['Location']).path,
+                         django.conf.settings.LOGIN_URL)
+
+    def test_redirects_user_to_oauth_authorize(self):
+        request = self.factory.get('/test')
+        request.session = self.session
+        request.user = User.objects.create_user(
+            username='bill3', email='bill@example.com', password='hunter2')
+
+        @decorators.oauth_required
+        def test_view(request):
+            return http.HttpResponse("test")  # pragma: NO COVER
+
+        response = test_view(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+        self.assertEqual(parse.urlparse(response['Location']).path,
+                         '/oauth2/oauth2authorize/')
diff --git a/tests/contrib/django_util/test_django_models.py b/tests/contrib/django_util/test_django_models.py
new file mode 100644
index 0000000..aeaed15
--- /dev/null
+++ b/tests/contrib/django_util/test_django_models.py
@@ -0,0 +1,99 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Django model tests.
+
+Unit tests for models and fields defined by the django_util helper.
+"""
+
+import base64
+import pickle
+
+from tests.contrib.django_util.models import CredentialsModel
+
+import unittest2
+
+from oauth2client._helpers import _from_bytes
+from oauth2client.client import Credentials
+from oauth2client.contrib.django_util.models import CredentialsField
+
+
+class TestCredentialsField(unittest2.TestCase):
+
+    def setUp(self):
+        self.fake_model = CredentialsModel()
+        self.fake_model_field = self.fake_model._meta.get_field('credentials')
+        self.field = CredentialsField(null=True)
+        self.credentials = Credentials()
+        self.pickle_str = _from_bytes(
+            base64.b64encode(pickle.dumps(self.credentials)))
+
+    def test_field_is_text(self):
+        self.assertEqual(self.field.get_internal_type(), 'BinaryField')
+
+    def test_field_unpickled(self):
+        self.assertIsInstance(
+            self.field.to_python(self.pickle_str), Credentials)
+
+    def test_field_already_unpickled(self):
+        self.assertIsInstance(
+            self.field.to_python(self.credentials), Credentials)
+
+    def test_none_field_unpickled(self):
+        self.assertIsNone(self.field.to_python(None))
+
+    def test_from_db_value(self):
+        value = self.field.from_db_value(
+            self.pickle_str, None, None, None)
+        self.assertIsInstance(value, Credentials)
+
+    def test_field_unpickled_none(self):
+        self.assertEqual(self.field.to_python(None), None)
+
+    def test_field_pickled(self):
+        prep_value = self.field.get_db_prep_value(self.credentials,
+                                                  connection=None)
+        self.assertEqual(prep_value, self.pickle_str)
+
+    def test_field_value_to_string(self):
+        self.fake_model.credentials = self.credentials
+        value_str = self.fake_model_field.value_to_string(self.fake_model)
+        self.assertEqual(value_str, self.pickle_str)
+
+    def test_field_value_to_string_none(self):
+        self.fake_model.credentials = None
+        value_str = self.fake_model_field.value_to_string(self.fake_model)
+        self.assertIsNone(value_str)
+
+    def test_credentials_without_null(self):
+        credentials = CredentialsField()
+        self.assertTrue(credentials.null)
+
+
+class CredentialWithSetStore(CredentialsField):
+    def __init__(self):
+        self.model = CredentialWithSetStore
+
+    def set_store(self, storage):
+        pass  # pragma: NO COVER
+
+
+class FakeCredentialsModelMock(object):
+
+    credentials = CredentialWithSetStore()
+
+
+class FakeCredentialsModelMockNoSet(object):
+
+    credentials = CredentialsField()
diff --git a/tests/contrib/django_util/test_django_storage.py b/tests/contrib/django_util/test_django_storage.py
new file mode 100644
index 0000000..8f76b18
--- /dev/null
+++ b/tests/contrib/django_util/test_django_storage.py
@@ -0,0 +1,164 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Tests for the DjangoORM storage class."""
+
+# Mock a Django environment
+import datetime
+
+from django.db import models
+import mock
+import unittest2
+
+from oauth2client import GOOGLE_TOKEN_URI
+from oauth2client.client import OAuth2Credentials
+from oauth2client.contrib.django_util.models import CredentialsField
+from oauth2client.contrib.django_util.storage import (
+    DjangoORMStorage as Storage)
+
+
+class TestStorage(unittest2.TestCase):
+    def setUp(self):
+        access_token = 'foo'
+        client_id = 'some_client_id'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = datetime.datetime.utcnow()
+        user_agent = 'refresh_checker/1.0'
+
+        self.credentials = OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, GOOGLE_TOKEN_URI,
+            user_agent)
+
+        self.key_name = 'id'
+        self.key_value = '1'
+        self.property_name = 'credentials'
+
+    def test_constructor(self):
+        storage = Storage(FakeCredentialsModel, self.key_name,
+                          self.key_value, self.property_name)
+
+        self.assertEqual(storage.model_class, FakeCredentialsModel)
+        self.assertEqual(storage.key_name, self.key_name)
+        self.assertEqual(storage.key_value, self.key_value)
+        self.assertEqual(storage.property_name, self.property_name)
+
+    @mock.patch('django.db.models')
+    def test_locked_get(self, djangoModel):
+        fake_model_with_credentials = FakeCredentialsModelMock()
+        entities = [
+            fake_model_with_credentials
+        ]
+        filter_mock = mock.Mock(return_value=entities)
+        object_mock = mock.Mock()
+        object_mock.filter = filter_mock
+        FakeCredentialsModelMock.objects = object_mock
+
+        storage = Storage(FakeCredentialsModelMock, self.key_name,
+                          self.key_value, self.property_name)
+        credential = storage.locked_get()
+        self.assertEqual(
+            credential, fake_model_with_credentials.credentials)
+
+    @mock.patch('django.db.models')
+    def test_locked_get_no_entities(self, djangoModel):
+        entities = []
+        filter_mock = mock.Mock(return_value=entities)
+        object_mock = mock.Mock()
+        object_mock.filter = filter_mock
+        FakeCredentialsModelMock.objects = object_mock
+
+        storage = Storage(FakeCredentialsModelMock, self.key_name,
+                          self.key_value, self.property_name)
+        credential = storage.locked_get()
+        self.assertIsNone(credential)
+
+    @mock.patch('django.db.models')
+    def test_locked_get_no_set_store(self, djangoModel):
+        fake_model_with_credentials = FakeCredentialsModelMockNoSet()
+        entities = [
+            fake_model_with_credentials
+        ]
+        filter_mock = mock.Mock(return_value=entities)
+        object_mock = mock.Mock()
+        object_mock.filter = filter_mock
+        FakeCredentialsModelMockNoSet.objects = object_mock
+
+        storage = Storage(FakeCredentialsModelMockNoSet, self.key_name,
+                          self.key_value, self.property_name)
+        credential = storage.locked_get()
+        self.assertEqual(
+            credential, fake_model_with_credentials.credentials)
+
+    @mock.patch('django.db.models')
+    def test_locked_put(self, djangoModel):
+        entity_mock = mock.Mock(credentials=None)
+        objects = mock.Mock(
+            get_or_create=mock.Mock(return_value=(entity_mock, None)))
+        FakeCredentialsModelMock.objects = objects
+        storage = Storage(FakeCredentialsModelMock, self.key_name,
+                          self.key_value, self.property_name)
+        storage.locked_put(self.credentials)
+
+    @mock.patch('django.db.models')
+    def test_locked_delete(self, djangoModel):
+        class FakeEntities(object):
+            def __init__(self):
+                self.deleted = False
+
+            def delete(self):
+                self.deleted = True
+
+        fake_entities = FakeEntities()
+        entities = fake_entities
+
+        filter_mock = mock.Mock(return_value=entities)
+        object_mock = mock.Mock()
+        object_mock.filter = filter_mock
+        FakeCredentialsModelMock.objects = object_mock
+        storage = Storage(FakeCredentialsModelMock, self.key_name,
+                          self.key_value, self.property_name)
+        storage.locked_delete()
+        self.assertTrue(fake_entities.deleted)
+
+
+class CredentialWithSetStore(CredentialsField):
+    def __init__(self):
+        self.model = CredentialWithSetStore
+
+    def set_store(self, storage):
+        pass
+
+
+class FakeCredentialsModel(models.Model):
+    credentials = CredentialsField()
+
+
+class FakeCredentialsModelMock(object):
+    def __init__(self, *args, **kwargs):
+        self.model = FakeCredentialsModelMock
+        self.saved = False
+        self.deleted = False
+
+    credentials = CredentialWithSetStore()
+
+
+class FakeCredentialsModelMockNoSet(object):
+    def __init__(self, set_store=False, *args, **kwargs):
+        self.model = FakeCredentialsModelMock
+        self.saved = False
+        self.deleted = False
+
+    credentials = CredentialsField()
diff --git a/tests/contrib/django_util/test_django_util.py b/tests/contrib/django_util/test_django_util.py
new file mode 100644
index 0000000..84457cb
--- /dev/null
+++ b/tests/contrib/django_util/test_django_util.py
@@ -0,0 +1,172 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Tests the initialization logic of django_util."""
+
+import copy
+
+import django.conf
+from django.conf.urls import include, url
+from django.contrib.auth.models import AnonymousUser
+from django.core import exceptions
+import mock
+from six.moves import reload_module
+from tests.contrib.django_util import TestWithDjangoEnvironment
+import unittest2
+
+from oauth2client.contrib import django_util
+import oauth2client.contrib.django_util
+from oauth2client.contrib.django_util import (
+    _CREDENTIALS_KEY, get_storage, site, UserOAuth2)
+
+
+urlpatterns = [
+    url(r'^oauth2/', include(site.urls))
+]
+
+
+class OAuth2SetupTest(unittest2.TestCase):
+
+    def setUp(self):
+        self.save_settings = copy.deepcopy(django.conf.settings)
+        # OAuth2 Settings gets configured based on Django settings
+        # at import time, so in order for us to reload the settings
+        # we need to reload the module
+        reload_module(oauth2client.contrib.django_util)
+
+    def tearDown(self):
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    @mock.patch("oauth2client.contrib.django_util.clientsecrets")
+    def test_settings_initialize(self, clientsecrets):
+        django.conf.settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON = 'file.json'
+        clientsecrets.loadfile.return_value = (
+            clientsecrets.TYPE_WEB,
+            {
+                'client_id': 'myid',
+                'client_secret': 'hunter2'
+            }
+        )
+
+        oauth2_settings = django_util.OAuth2Settings(django.conf.settings)
+        self.assertTrue(clientsecrets.loadfile.called)
+        self.assertEqual(oauth2_settings.client_id, 'myid')
+        self.assertEqual(oauth2_settings.client_secret, 'hunter2')
+        django.conf.settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON = None
+
+    @mock.patch("oauth2client.contrib.django_util.clientsecrets")
+    def test_settings_initialize_invalid_type(self, clientsecrets):
+        django.conf.settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON = 'file.json'
+        clientsecrets.loadfile.return_value = (
+            "wrong_type",
+            {
+                'client_id': 'myid',
+                'client_secret': 'hunter2'
+            }
+        )
+
+        with self.assertRaises(ValueError):
+            django_util.OAuth2Settings.__init__(
+                object.__new__(django_util.OAuth2Settings),
+                django.conf.settings)
+
+    @mock.patch("oauth2client.contrib.django_util.clientsecrets")
+    def test_no_settings(self, clientsecrets):
+        django.conf.settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON = None
+        django.conf.settings.GOOGLE_OAUTH2_CLIENT_SECRET = None
+        django.conf.settings.GOOGLE_OAUTH2_CLIENT_ID = None
+
+        with self.assertRaises(exceptions.ImproperlyConfigured):
+            django_util.OAuth2Settings.__init__(
+                object.__new__(django_util.OAuth2Settings),
+                django.conf.settings)
+
+    @mock.patch("oauth2client.contrib.django_util.clientsecrets")
+    def test_no_session_middleware(self, clientsecrets):
+        django.conf.settings.MIDDLEWARE_CLASSES = ()
+
+        with self.assertRaises(exceptions.ImproperlyConfigured):
+            django_util.OAuth2Settings.__init__(
+                object.__new__(django_util.OAuth2Settings),
+                django.conf.settings)
+
+    def test_storage_model(self):
+        STORAGE_MODEL = {
+            'model': 'tests.contrib.django_util.models.CredentialsModel',
+            'user_property': 'user_id',
+            'credentials_property': 'credentials'
+        }
+        django.conf.settings.GOOGLE_OAUTH2_STORAGE_MODEL = STORAGE_MODEL
+        oauth2_settings = django_util.OAuth2Settings(django.conf.settings)
+        self.assertEqual(oauth2_settings.storage_model, STORAGE_MODEL['model'])
+        self.assertEqual(oauth2_settings.storage_model_user_property,
+                         STORAGE_MODEL['user_property'])
+        self.assertEqual(oauth2_settings.storage_model_credentials_property,
+                         STORAGE_MODEL['credentials_property'])
+
+
+class MockObjectWithSession(object):
+    def __init__(self, session):
+        self.session = session
+
+
+class SessionStorageTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(SessionStorageTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+        reload_module(oauth2client.contrib.django_util)
+
+    def tearDown(self):
+        super(SessionStorageTest, self).tearDown()
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_session_delete(self):
+        self.session[_CREDENTIALS_KEY] = "test_val"
+        request = MockObjectWithSession(self.session)
+        django_storage = get_storage(request)
+        django_storage.delete()
+        self.assertIsNone(self.session.get(_CREDENTIALS_KEY))
+
+    def test_session_delete_nothing(self):
+        request = MockObjectWithSession(self.session)
+        django_storage = get_storage(request)
+        django_storage.delete()
+
+
+class TestUserOAuth2Object(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(TestUserOAuth2Object, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+        STORAGE_MODEL = {
+            'model': 'tests.contrib.django_util.models.CredentialsModel',
+            'user_property': 'user_id',
+            'credentials_property': 'credentials'
+        }
+        django.conf.settings.GOOGLE_OAUTH2_STORAGE_MODEL = STORAGE_MODEL
+        reload_module(oauth2client.contrib.django_util)
+
+    def tearDown(self):
+        super(TestUserOAuth2Object, self).tearDown()
+        import django.conf
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_get_credentials_anon_user(self):
+        request = self.factory.get('oauth2/oauth2authorize',
+                                   data={'return_url': '/return_endpoint'})
+        request.session = self.session
+        request.user = AnonymousUser()
+        oauth2 = UserOAuth2(request)
+        self.assertIsNone(oauth2.credentials)
diff --git a/tests/contrib/django_util/test_views.py b/tests/contrib/django_util/test_views.py
new file mode 100644
index 0000000..df0d11c
--- /dev/null
+++ b/tests/contrib/django_util/test_views.py
@@ -0,0 +1,274 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Unit test for django_util views"""
+
+import copy
+import json
+
+import django
+from django import http
+import django.conf
+from django.contrib.auth.models import AnonymousUser, User
+import mock
+from six.moves import reload_module
+
+from tests.contrib.django_util import TestWithDjangoEnvironment
+from tests.contrib.django_util.models import CredentialsModel
+
+from oauth2client.client import FlowExchangeError, OAuth2WebServerFlow
+import oauth2client.contrib.django_util
+from oauth2client.contrib.django_util import views
+from oauth2client.contrib.django_util.models import CredentialsField
+
+
+class OAuth2AuthorizeTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(OAuth2AuthorizeTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+        reload_module(oauth2client.contrib.django_util)
+        self.user = User.objects.create_user(
+          username='bill', email='bill@example.com', password='hunter2')
+
+    def tearDown(self):
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_authorize_works(self):
+        request = self.factory.get('oauth2/oauth2authorize')
+        request.session = self.session
+        request.user = self.user
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+
+    def test_authorize_anonymous_user(self):
+        request = self.factory.get('oauth2/oauth2authorize')
+        request.session = self.session
+        request.user = AnonymousUser()
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+
+    def test_authorize_works_explicit_return_url(self):
+        request = self.factory.get('oauth2/oauth2authorize',
+                                   data={'return_url': '/return_endpoint'})
+        request.session = self.session
+        request.user = self.user
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+
+
+class Oauth2AuthorizeStorageModelTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(Oauth2AuthorizeStorageModelTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+
+        STORAGE_MODEL = {
+            'model': 'tests.contrib.django_util.models.CredentialsModel',
+            'user_property': 'user_id',
+            'credentials_property': 'credentials'
+        }
+        django.conf.settings.GOOGLE_OAUTH2_STORAGE_MODEL = STORAGE_MODEL
+
+        # OAuth2 Settings gets configured based on Django settings
+        # at import time, so in order for us to reload the settings
+        # we need to reload the module
+        reload_module(oauth2client.contrib.django_util)
+        self.user = User.objects.create_user(
+            username='bill', email='bill@example.com', password='hunter2')
+
+    def tearDown(self):
+        django.conf.settings = copy.deepcopy(self.save_settings)
+
+    def test_authorize_works(self):
+        request = self.factory.get('oauth2/oauth2authorize')
+        request.session = self.session
+        request.user = self.user
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+        # redirects to Google oauth
+        self.assertIn('accounts.google.com', response.url)
+
+    def test_authorize_anonymous_user_redirects_login(self):
+        request = self.factory.get('oauth2/oauth2authorize')
+        request.session = self.session
+        request.user = AnonymousUser()
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+        # redirects to Django login
+        self.assertIn(django.conf.settings.LOGIN_URL, response.url)
+
+    def test_authorize_works_explicit_return_url(self):
+        request = self.factory.get('oauth2/oauth2authorize',
+                                   data={'return_url': '/return_endpoint'})
+        request.session = self.session
+        request.user = self.user
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+
+    def test_authorized_user_not_logged_in_redirects(self):
+        request = self.factory.get('oauth2/oauth2authorize',
+                                   data={'return_url': '/return_endpoint'})
+        request.session = self.session
+
+        authorized_user = User.objects.create_user(
+            username='bill2', email='bill@example.com', password='hunter2')
+        credentials = CredentialsField()
+
+        CredentialsModel.objects.create(
+            user_id=authorized_user,
+            credentials=credentials)
+
+        request.user = authorized_user
+        response = views.oauth2_authorize(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+
+
+class Oauth2CallbackTest(TestWithDjangoEnvironment):
+
+    def setUp(self):
+        super(Oauth2CallbackTest, self).setUp()
+        self.save_settings = copy.deepcopy(django.conf.settings)
+        reload_module(oauth2client.contrib.django_util)
+
+        self.CSRF_TOKEN = 'token'
+        self.RETURN_URL = 'http://return-url.com'
+        self.fake_state = {
+            'csrf_token': self.CSRF_TOKEN,
+            'return_url': self.RETURN_URL,
+            'scopes': django.conf.settings.GOOGLE_OAUTH2_SCOPES
+        }
+        self.user = User.objects.create_user(
+            username='bill', email='bill@example.com', password='hunter2')
+
+    @mock.patch('oauth2client.contrib.django_util.views.pickle')
+    def test_callback_works(self, pickle):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            'state': json.dumps(self.fake_state),
+            'code': 123
+        })
+
+        self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN
+
+        flow = OAuth2WebServerFlow(
+            client_id='clientid',
+            client_secret='clientsecret',
+            scope=['email'],
+            state=json.dumps(self.fake_state),
+            redirect_uri=request.build_absolute_uri("oauth2/oauth2callback"))
+
+        name = 'google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)
+        self.session[name] = pickle.dumps(flow)
+        flow.step2_exchange = mock.Mock()
+        pickle.loads.return_value = flow
+
+        request.session = self.session
+        request.user = self.user
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseRedirect)
+        self.assertEqual(
+            response.status_code, django.http.HttpResponseRedirect.status_code)
+        self.assertEqual(response['Location'], self.RETURN_URL)
+
+    @mock.patch('oauth2client.contrib.django_util.views.pickle')
+    def test_callback_handles_bad_flow_exchange(self, pickle):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            "state": json.dumps(self.fake_state),
+            "code": 123
+        })
+
+        self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN
+
+        flow = OAuth2WebServerFlow(
+            client_id='clientid',
+            client_secret='clientsecret',
+            scope=['email'],
+            state=json.dumps(self.fake_state),
+            redirect_uri=request.build_absolute_uri('oauth2/oauth2callback'))
+
+        self.session['google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)] \
+            = pickle.dumps(flow)
+
+        def local_throws(code):
+            raise FlowExchangeError('test')
+
+        flow.step2_exchange = local_throws
+        pickle.loads.return_value = flow
+
+        request.session = self.session
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+
+    def test_error_returns_bad_request(self):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            'error': 'There was an error in your authorization.',
+        })
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+        self.assertIn(b'Authorization failed', response.content)
+
+    def test_no_session(self):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            'code': 123,
+            'state': json.dumps(self.fake_state)
+        })
+
+        request.session = self.session
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+        self.assertEqual(
+            response.content, b'No existing session for this flow.')
+
+    def test_missing_state_returns_bad_request(self):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            'code': 123
+        })
+        self.session['google_oauth2_csrf_token'] = "token"
+        request.session = self.session
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+
+    def test_bad_state(self):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            'code': 123,
+            'state': json.dumps({'wrong': 'state'})
+        })
+        self.session['google_oauth2_csrf_token'] = 'token'
+        request.session = self.session
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+        self.assertEqual(response.content, b'Invalid state parameter.')
+
+    def test_bad_csrf(self):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            "state": json.dumps(self.fake_state),
+            "code": 123
+        })
+        self.session['google_oauth2_csrf_token'] = 'WRONG TOKEN'
+        request.session = self.session
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+        self.assertEqual(response.content, b'Invalid CSRF token.')
+
+    def test_no_saved_flow(self):
+        request = self.factory.get('oauth2/oauth2callback', data={
+            'state': json.dumps(self.fake_state),
+            'code': 123
+        })
+        self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN
+        self.session['google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)] = None
+        request.session = self.session
+        response = views.oauth2_callback(request)
+        self.assertIsInstance(response, http.HttpResponseBadRequest)
+        self.assertEqual(response.content, b'Missing Oauth2 flow.')
diff --git a/tests/contrib/test__appengine_ndb.py b/tests/contrib/test__appengine_ndb.py
new file mode 100644
index 0000000..41e3805
--- /dev/null
+++ b/tests/contrib/test__appengine_ndb.py
@@ -0,0 +1,166 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import json
+import os
+
+from google.appengine.ext import ndb
+from google.appengine.ext import testbed
+import mock
+import unittest2
+
+from oauth2client import client
+from oauth2client.contrib import appengine
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
+
+
+def datafile(filename):
+    return os.path.join(DATA_DIR, filename)
+
+
+class TestNDBModel(ndb.Model):
+    flow = appengine.FlowNDBProperty()
+    creds = appengine.CredentialsNDBProperty()
+
+
+class TestFlowNDBProperty(unittest2.TestCase):
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_flow_get_put(self):
+        instance = TestNDBModel(
+            flow=client.flow_from_clientsecrets(
+                datafile('client_secrets.json'), 'foo', redirect_uri='oob'),
+            id='foo'
+        )
+        instance.put()
+        retrieved = TestNDBModel.get_by_id('foo')
+
+        self.assertEqual('foo_client_id', retrieved.flow.client_id)
+
+    @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER')
+    def test_validate_success(self, mock_logger):
+        flow_prop = TestNDBModel.flow
+        flow_val = client.flow_from_clientsecrets(
+            datafile('client_secrets.json'), 'foo', redirect_uri='oob')
+        flow_prop._validate(flow_val)
+        mock_logger.info.assert_called_once_with('validate: Got type %s',
+                                                 type(flow_val))
+
+    @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER')
+    def test_validate_none(self, mock_logger):
+        flow_prop = TestNDBModel.flow
+        flow_val = None
+        flow_prop._validate(flow_val)
+        mock_logger.info.assert_called_once_with('validate: Got type %s',
+                                                 type(flow_val))
+
+    @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER')
+    def test_validate_bad_type(self, mock_logger):
+        flow_prop = TestNDBModel.flow
+        flow_val = object()
+        with self.assertRaises(TypeError):
+            flow_prop._validate(flow_val)
+        mock_logger.info.assert_called_once_with('validate: Got type %s',
+                                                 type(flow_val))
+
+
+class TestCredentialsNDBProperty(unittest2.TestCase):
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_valid_creds_get_put(self):
+        creds = client.Credentials()
+        instance = TestNDBModel(creds=creds, id='bar')
+        instance.put()
+        retrieved = TestNDBModel.get_by_id('bar')
+        self.assertIsInstance(retrieved.creds, client.Credentials)
+
+    @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER')
+    def test_validate_success(self, mock_logger):
+        creds_prop = TestNDBModel.creds
+        creds_val = client.Credentials()
+        creds_prop._validate(creds_val)
+        mock_logger.info.assert_called_once_with('validate: Got type %s',
+                                                 type(creds_val))
+
+    @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER')
+    def test_validate_none(self, mock_logger):
+        creds_prop = TestNDBModel.creds
+        creds_val = None
+        creds_prop._validate(creds_val)
+        mock_logger.info.assert_called_once_with('validate: Got type %s',
+                                                 type(creds_val))
+
+    @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER')
+    def test_validate_bad_type(self, mock_logger):
+        creds_prop = TestNDBModel.creds
+        creds_val = object()
+        with self.assertRaises(TypeError):
+            creds_prop._validate(creds_val)
+        mock_logger.info.assert_called_once_with('validate: Got type %s',
+                                                 type(creds_val))
+
+    def test__to_base_type_valid_creds(self):
+        creds_prop = TestNDBModel.creds
+        creds = client.Credentials()
+        creds_json = json.loads(creds_prop._to_base_type(creds))
+        self.assertDictEqual(creds_json, {
+            '_class': 'Credentials',
+            '_module': 'oauth2client.client',
+            'token_expiry': None,
+        })
+
+    def test__to_base_type_null_creds(self):
+        creds_prop = TestNDBModel.creds
+        self.assertEqual(creds_prop._to_base_type(None), '')
+
+    def test__from_base_type_valid_creds(self):
+        creds_prop = TestNDBModel.creds
+        creds_json = json.dumps({
+            '_class': 'Credentials',
+            '_module': 'oauth2client.client',
+            'token_expiry': None,
+        })
+        creds = creds_prop._from_base_type(creds_json)
+        self.assertIsInstance(creds, client.Credentials)
+
+    def test__from_base_type_false_value(self):
+        creds_prop = TestNDBModel.creds
+        self.assertIsNone(creds_prop._from_base_type(''))
+        self.assertIsNone(creds_prop._from_base_type(False))
+        self.assertIsNone(creds_prop._from_base_type(None))
+        self.assertIsNone(creds_prop._from_base_type([]))
+        self.assertIsNone(creds_prop._from_base_type({}))
+
+    def test__from_base_type_bad_json(self):
+        creds_prop = TestNDBModel.creds
+        creds_json = '{JK-I-AM-NOT-JSON'
+        self.assertIsNone(creds_prop._from_base_type(creds_json))
diff --git a/tests/contrib/test_appengine.py b/tests/contrib/test_appengine.py
new file mode 100644
index 0000000..cdaf6c5
--- /dev/null
+++ b/tests/contrib/test_appengine.py
@@ -0,0 +1,1073 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+import datetime
+import json
+import os
+import tempfile
+import time
+
+import dev_appserver
+
+dev_appserver.fix_sys_path()
+
+from google.appengine.api import apiproxy_stub
+from google.appengine.api import apiproxy_stub_map
+from google.appengine.api import app_identity
+from google.appengine.api import memcache
+from google.appengine.api import users
+from google.appengine.api.memcache import memcache_stub
+from google.appengine.ext import db
+from google.appengine.ext import ndb
+from google.appengine.ext import testbed
+import httplib2
+import mock
+from six.moves import urllib
+import unittest2
+import webapp2
+from webtest import TestApp
+
+import oauth2client
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client.contrib import appengine
+from ..http_mock import CacheMock
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
+
+
+def datafile(filename):
+    return os.path.join(DATA_DIR, filename)
+
+
+def load_and_cache(existing_file, fakename, cache_mock):
+    client_type, client_info = clientsecrets._loadfile(datafile(existing_file))
+    cache_mock.cache[fakename] = {client_type: client_info}
+
+
+class UserMock(object):
+    """Mock the app engine user service"""
+
+    def __call__(self):
+        return self
+
+    def user_id(self):
+        return 'foo_user'
+
+
+class UserNotLoggedInMock(object):
+    """Mock the app engine user service"""
+
+    def __call__(self):
+        return None
+
+
+class Http2Mock(object):
+    """Mock httplib2.Http"""
+    status = 200
+    content = {
+        'access_token': 'foo_access_token',
+        'refresh_token': 'foo_refresh_token',
+        'expires_in': 3600,
+        'extra': 'value',
+    }
+
+    def request(self, token_uri, method, body, headers, *args, **kwargs):
+        self.body = body
+        self.headers = headers
+        return self, json.dumps(self.content)
+
+
+class TestAppAssertionCredentials(unittest2.TestCase):
+    account_name = "service_account_name@appspot.com"
+    signature = "signature"
+
+    class AppIdentityStubImpl(apiproxy_stub.APIProxyStub):
+
+        def __init__(self, key_name=None, sig_bytes=None,
+                     svc_acct=None):
+            super(TestAppAssertionCredentials.AppIdentityStubImpl,
+                  self).__init__('app_identity_service')
+            self._key_name = key_name
+            self._sig_bytes = sig_bytes
+            self._sign_calls = []
+            self._svc_acct = svc_acct
+            self._get_acct_name_calls = 0
+
+        def _Dynamic_GetAccessToken(self, request, response):
+            response.set_access_token('a_token_123')
+            response.set_expiration_time(time.time() + 1800)
+
+        def _Dynamic_SignForApp(self, request, response):
+            response.set_key_name(self._key_name)
+            response.set_signature_bytes(self._sig_bytes)
+            self._sign_calls.append(request.bytes_to_sign())
+
+        def _Dynamic_GetServiceAccountName(self, request, response):
+            response.set_service_account_name(self._svc_acct)
+            self._get_acct_name_calls += 1
+
+    class ErroringAppIdentityStubImpl(apiproxy_stub.APIProxyStub):
+
+        def __init__(self):
+            super(TestAppAssertionCredentials.ErroringAppIdentityStubImpl,
+                  self).__init__('app_identity_service')
+
+        def _Dynamic_GetAccessToken(self, request, response):
+            raise app_identity.BackendDeadlineExceeded()
+
+    def test_raise_correct_type_of_exception(self):
+        app_identity_stub = self.ErroringAppIdentityStubImpl()
+        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+        apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
+                                                app_identity_stub)
+        apiproxy_stub_map.apiproxy.RegisterStub(
+            'memcache', memcache_stub.MemcacheServiceStub())
+
+        scope = 'http://www.googleapis.com/scope'
+        credentials = appengine.AppAssertionCredentials(scope)
+        http = httplib2.Http()
+        with self.assertRaises(client.AccessTokenRefreshError):
+            credentials.refresh(http)
+
+    def test_get_access_token_on_refresh(self):
+        app_identity_stub = self.AppIdentityStubImpl()
+        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+        apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
+                                                app_identity_stub)
+        apiproxy_stub_map.apiproxy.RegisterStub(
+            'memcache', memcache_stub.MemcacheServiceStub())
+
+        scope = [
+            "http://www.googleapis.com/scope",
+            "http://www.googleapis.com/scope2"]
+        credentials = appengine.AppAssertionCredentials(scope)
+        http = httplib2.Http()
+        credentials.refresh(http)
+        self.assertEqual('a_token_123', credentials.access_token)
+
+        json = credentials.to_json()
+        credentials = client.Credentials.new_from_json(json)
+        self.assertEqual(
+            'http://www.googleapis.com/scope http://www.googleapis.com/scope2',
+            credentials.scope)
+
+        scope = ('http://www.googleapis.com/scope '
+                 'http://www.googleapis.com/scope2')
+        credentials = appengine.AppAssertionCredentials(scope)
+        http = httplib2.Http()
+        credentials.refresh(http)
+        self.assertEqual('a_token_123', credentials.access_token)
+        self.assertEqual(
+            'http://www.googleapis.com/scope http://www.googleapis.com/scope2',
+            credentials.scope)
+
+    def test_custom_service_account(self):
+        scope = "http://www.googleapis.com/scope"
+        account_id = "service_account_name_2@appspot.com"
+
+        with mock.patch.object(app_identity, 'get_access_token',
+                               return_value=('a_token_456', None),
+                               autospec=True) as get_access_token:
+            credentials = appengine.AppAssertionCredentials(
+                scope, service_account_id=account_id)
+            http = httplib2.Http()
+            credentials.refresh(http)
+
+            self.assertEqual('a_token_456', credentials.access_token)
+            self.assertEqual(scope, credentials.scope)
+            get_access_token.assert_called_once_with(
+                [scope], service_account_id=account_id)
+
+    def test_create_scoped_required_without_scopes(self):
+        credentials = appengine.AppAssertionCredentials([])
+        self.assertTrue(credentials.create_scoped_required())
+
+    def test_create_scoped_required_with_scopes(self):
+        credentials = appengine.AppAssertionCredentials(['dummy_scope'])
+        self.assertFalse(credentials.create_scoped_required())
+
+    def test_create_scoped(self):
+        credentials = appengine.AppAssertionCredentials([])
+        new_credentials = credentials.create_scoped(['dummy_scope'])
+        self.assertNotEqual(credentials, new_credentials)
+        self.assertIsInstance(
+            new_credentials, appengine.AppAssertionCredentials)
+        self.assertEqual('dummy_scope', new_credentials.scope)
+
+    def test_sign_blob(self):
+        key_name = b'1234567890'
+        sig_bytes = b'himom'
+        app_identity_stub = self.AppIdentityStubImpl(
+            key_name=key_name, sig_bytes=sig_bytes)
+        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+        apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
+                                                app_identity_stub)
+        credentials = appengine.AppAssertionCredentials([])
+        to_sign = b'blob'
+        self.assertEqual(app_identity_stub._sign_calls, [])
+        result = credentials.sign_blob(to_sign)
+        self.assertEqual(result, (key_name, sig_bytes))
+        self.assertEqual(app_identity_stub._sign_calls, [to_sign])
+
+    def test_service_account_email(self):
+        acct_name = 'new-value@appspot.gserviceaccount.com'
+        app_identity_stub = self.AppIdentityStubImpl(svc_acct=acct_name)
+        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+        apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
+                                                app_identity_stub)
+
+        credentials = appengine.AppAssertionCredentials([])
+        self.assertIsNone(credentials._service_account_email)
+        self.assertEqual(app_identity_stub._get_acct_name_calls, 0)
+        self.assertEqual(credentials.service_account_email, acct_name)
+        self.assertIsNotNone(credentials._service_account_email)
+        self.assertEqual(app_identity_stub._get_acct_name_calls, 1)
+
+    def test_service_account_email_already_set(self):
+        acct_name = 'existing@appspot.gserviceaccount.com'
+        credentials = appengine.AppAssertionCredentials([])
+        credentials._service_account_email = acct_name
+
+        app_identity_stub = self.AppIdentityStubImpl(svc_acct=acct_name)
+        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+        apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
+                                                app_identity_stub)
+
+        self.assertEqual(app_identity_stub._get_acct_name_calls, 0)
+        self.assertEqual(credentials.service_account_email, acct_name)
+        self.assertEqual(app_identity_stub._get_acct_name_calls, 0)
+
+    def test_get_access_token(self):
+        app_identity_stub = self.AppIdentityStubImpl()
+        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+        apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
+                                                app_identity_stub)
+        apiproxy_stub_map.apiproxy.RegisterStub(
+            'memcache', memcache_stub.MemcacheServiceStub())
+
+        credentials = appengine.AppAssertionCredentials(['dummy_scope'])
+        token = credentials.get_access_token()
+        self.assertEqual('a_token_123', token.access_token)
+        self.assertEqual(None, token.expires_in)
+
+    def test_save_to_well_known_file(self):
+        os.environ[client._CLOUDSDK_CONFIG_ENV_VAR] = tempfile.mkdtemp()
+        credentials = appengine.AppAssertionCredentials([])
+        with self.assertRaises(NotImplementedError):
+            client.save_to_well_known_file(credentials)
+        del os.environ[client._CLOUDSDK_CONFIG_ENV_VAR]
+
+
+class TestFlowModel(db.Model):
+    flow = appengine.FlowProperty()
+
+
+class FlowPropertyTest(unittest2.TestCase):
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+
+        self.flow = client.flow_from_clientsecrets(
+            datafile('client_secrets.json'),
+            'foo',
+            redirect_uri='oob')
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_flow_get_put(self):
+        instance = TestFlowModel(
+            flow=self.flow,
+            key_name='foo'
+        )
+        instance.put()
+        retrieved = TestFlowModel.get_by_key_name('foo')
+
+        self.assertEqual('foo_client_id', retrieved.flow.client_id)
+
+    def test_make_value_from_datastore_none(self):
+        self.assertIsNone(
+            appengine.FlowProperty().make_value_from_datastore(None))
+
+    def test_validate(self):
+        appengine.FlowProperty().validate(None)
+        with self.assertRaises(db.BadValueError):
+            appengine.FlowProperty().validate(42)
+
+
+class TestCredentialsModel(db.Model):
+    credentials = appengine.CredentialsProperty()
+
+
+class CredentialsPropertyTest(unittest2.TestCase):
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+
+        access_token = 'foo'
+        client_id = 'some_client_id'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = datetime.datetime.utcnow()
+        user_agent = 'refresh_checker/1.0'
+        self.credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, oauth2client.GOOGLE_TOKEN_URI,
+            user_agent)
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_credentials_get_put(self):
+        instance = TestCredentialsModel(
+            credentials=self.credentials,
+            key_name='foo'
+        )
+        instance.put()
+        retrieved = TestCredentialsModel.get_by_key_name('foo')
+
+        self.assertEqual(
+            self.credentials.to_json(),
+            retrieved.credentials.to_json())
+
+    def test_make_value_from_datastore(self):
+        self.assertIsNone(
+            appengine.CredentialsProperty().make_value_from_datastore(None))
+        self.assertIsNone(
+            appengine.CredentialsProperty().make_value_from_datastore(''))
+        self.assertIsNone(
+            appengine.CredentialsProperty().make_value_from_datastore('{'))
+
+        decoded = appengine.CredentialsProperty().make_value_from_datastore(
+            self.credentials.to_json())
+        self.assertEqual(
+            self.credentials.to_json(),
+            decoded.to_json())
+
+    def test_validate(self):
+        appengine.CredentialsProperty().validate(self.credentials)
+        appengine.CredentialsProperty().validate(None)
+        with self.assertRaises(db.BadValueError):
+            appengine.CredentialsProperty().validate(42)
+
+
+def _http_request(*args, **kwargs):
+    resp = httplib2.Response({'status': '200'})
+    content = json.dumps({'access_token': 'bar'})
+
+    return resp, content
+
+
+class StorageByKeyNameTest(unittest2.TestCase):
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+        self.testbed.init_user_stub()
+
+        access_token = 'foo'
+        client_id = 'some_client_id'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = datetime.datetime.utcnow()
+        user_agent = 'refresh_checker/1.0'
+        self.credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, oauth2client.GOOGLE_TOKEN_URI,
+            user_agent)
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_bad_ctor(self):
+        with self.assertRaises(ValueError):
+            appengine.StorageByKeyName(appengine.CredentialsModel, None, None)
+
+    def test__is_ndb(self):
+        storage = appengine.StorageByKeyName(
+            object(), 'foo', 'credentials')
+
+        with self.assertRaises(TypeError):
+            storage._is_ndb()
+
+        storage._model = type(object)
+        with self.assertRaises(TypeError):
+            storage._is_ndb()
+
+        storage._model = appengine.CredentialsModel
+        self.assertFalse(storage._is_ndb())
+
+        storage._model = appengine.CredentialsNDBModel
+        self.assertTrue(storage._is_ndb())
+
+    def test_get_and_put_simple(self):
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsModel, 'foo', 'credentials')
+
+        self.assertEqual(None, storage.get())
+        self.credentials.set_store(storage)
+
+        self.credentials._refresh(_http_request)
+        credmodel = appengine.CredentialsModel.get_by_key_name('foo')
+        self.assertEqual('bar', credmodel.credentials.access_token)
+
+    def test_get_and_put_cached(self):
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsModel, 'foo', 'credentials', cache=memcache)
+
+        self.assertEqual(None, storage.get())
+        self.credentials.set_store(storage)
+
+        self.credentials._refresh(_http_request)
+        credmodel = appengine.CredentialsModel.get_by_key_name('foo')
+        self.assertEqual('bar', credmodel.credentials.access_token)
+
+        # Now remove the item from the cache.
+        memcache.delete('foo')
+
+        # Check that getting refreshes the cache.
+        credentials = storage.get()
+        self.assertEqual('bar', credentials.access_token)
+        self.assertNotEqual(None, memcache.get('foo'))
+
+        # Deleting should clear the cache.
+        storage.delete()
+        credentials = storage.get()
+        self.assertEqual(None, credentials)
+        self.assertEqual(None, memcache.get('foo'))
+
+    def test_get_and_put_set_store_on_cache_retrieval(self):
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsModel, 'foo', 'credentials', cache=memcache)
+
+        self.assertEqual(None, storage.get())
+        self.credentials.set_store(storage)
+        storage.put(self.credentials)
+        # Pre-bug 292 old_creds wouldn't have storage, and the _refresh
+        # wouldn't be able to store the updated cred back into the storage.
+        old_creds = storage.get()
+        self.assertEqual(old_creds.access_token, 'foo')
+        old_creds.invalid = True
+        old_creds._refresh(_http_request)
+        new_creds = storage.get()
+        self.assertEqual(new_creds.access_token, 'bar')
+
+    def test_get_and_put_ndb(self):
+        # Start empty
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsNDBModel, 'foo', 'credentials')
+        self.assertEqual(None, storage.get())
+
+        # Refresh storage and retrieve without using storage
+        self.credentials.set_store(storage)
+        self.credentials._refresh(_http_request)
+        credmodel = appengine.CredentialsNDBModel.get_by_id('foo')
+        self.assertEqual('bar', credmodel.credentials.access_token)
+        self.assertEqual(credmodel.credentials.to_json(),
+                         self.credentials.to_json())
+
+    def test_delete_ndb(self):
+        # Start empty
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsNDBModel, 'foo', 'credentials')
+        self.assertEqual(None, storage.get())
+
+        # Add credentials to model with storage, and check equivalent
+        # w/o storage
+        storage.put(self.credentials)
+        credmodel = appengine.CredentialsNDBModel.get_by_id('foo')
+        self.assertEqual(credmodel.credentials.to_json(),
+                         self.credentials.to_json())
+
+        # Delete and make sure empty
+        storage.delete()
+        self.assertEqual(None, storage.get())
+
+    def test_get_and_put_mixed_ndb_storage_db_get(self):
+        # Start empty
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsNDBModel, 'foo', 'credentials')
+        self.assertEqual(None, storage.get())
+
+        # Set NDB store and refresh to add to storage
+        self.credentials.set_store(storage)
+        self.credentials._refresh(_http_request)
+
+        # Retrieve same key from DB model to confirm mixing works
+        credmodel = appengine.CredentialsModel.get_by_key_name('foo')
+        self.assertEqual('bar', credmodel.credentials.access_token)
+        self.assertEqual(self.credentials.to_json(),
+                         credmodel.credentials.to_json())
+
+    def test_get_and_put_mixed_db_storage_ndb_get(self):
+        # Start empty
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsModel, 'foo', 'credentials')
+        self.assertEqual(None, storage.get())
+
+        # Set DB store and refresh to add to storage
+        self.credentials.set_store(storage)
+        self.credentials._refresh(_http_request)
+
+        # Retrieve same key from NDB model to confirm mixing works
+        credmodel = appengine.CredentialsNDBModel.get_by_id('foo')
+        self.assertEqual('bar', credmodel.credentials.access_token)
+        self.assertEqual(self.credentials.to_json(),
+                         credmodel.credentials.to_json())
+
+    def test_delete_db_ndb_mixed(self):
+        # Start empty
+        storage_ndb = appengine.StorageByKeyName(
+            appengine.CredentialsNDBModel, 'foo', 'credentials')
+        storage = appengine.StorageByKeyName(
+            appengine.CredentialsModel, 'foo', 'credentials')
+
+        # First DB, then NDB
+        self.assertEqual(None, storage.get())
+        storage.put(self.credentials)
+        self.assertNotEqual(None, storage.get())
+
+        storage_ndb.delete()
+        self.assertEqual(None, storage.get())
+
+        # First NDB, then DB
+        self.assertEqual(None, storage_ndb.get())
+        storage_ndb.put(self.credentials)
+
+        storage.delete()
+        self.assertNotEqual(None, storage_ndb.get())
+        # NDB uses memcache and an instance cache (Context)
+        ndb.get_context().clear_cache()
+        memcache.flush_all()
+        self.assertEqual(None, storage_ndb.get())
+
+
+class MockRequest(object):
+    url = 'https://example.org'
+
+    def relative_url(self, rel):
+        return self.url + rel
+
+
+class MockRequestHandler(object):
+    request = MockRequest()
+
+
+class DecoratorTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+        self.testbed.init_user_stub()
+
+        decorator = appengine.OAuth2Decorator(
+            client_id='foo_client_id', client_secret='foo_client_secret',
+            scope=['foo_scope', 'bar_scope'], user_agent='foo')
+
+        self._finish_setup(decorator, user_mock=UserMock)
+
+    def _finish_setup(self, decorator, user_mock):
+        self.decorator = decorator
+        self.had_credentials = False
+        self.found_credentials = None
+        self.should_raise = False
+        parent = self
+
+        class TestRequiredHandler(webapp2.RequestHandler):
+            @decorator.oauth_required
+            def get(self):
+                parent.assertTrue(decorator.has_credentials())
+                parent.had_credentials = True
+                parent.found_credentials = decorator.credentials
+                if parent.should_raise:
+                    raise parent.should_raise
+
+        class TestAwareHandler(webapp2.RequestHandler):
+            @decorator.oauth_aware
+            def get(self, *args, **kwargs):
+                self.response.out.write('Hello World!')
+                assert(kwargs['year'] == '2012')
+                assert(kwargs['month'] == '01')
+                if decorator.has_credentials():
+                    parent.had_credentials = True
+                    parent.found_credentials = decorator.credentials
+                if parent.should_raise:
+                    raise parent.should_raise
+
+        routes = [
+            ('/oauth2callback', self.decorator.callback_handler()),
+            ('/foo_path', TestRequiredHandler),
+            webapp2.Route(r'/bar_path/<year:\d{4}>/<month:\d{2}>',
+                          handler=TestAwareHandler, name='bar'),
+        ]
+        application = webapp2.WSGIApplication(routes, debug=True)
+
+        self.app = TestApp(application, extra_environ={
+            'wsgi.url_scheme': 'http',
+            'HTTP_HOST': 'localhost',
+        })
+        self.current_user = user_mock()
+        users.get_current_user = self.current_user
+        self.httplib2_orig = httplib2.Http
+        httplib2.Http = Http2Mock
+
+    def tearDown(self):
+        self.testbed.deactivate()
+        httplib2.Http = self.httplib2_orig
+
+    def test_in_error(self):
+        # NOTE: This branch is never reached. _in_error is not set by any code
+        # path. It appears to be intended to be set during construction.
+        self.decorator._in_error = True
+        self.decorator._message = 'foobar'
+
+        response = self.app.get('http://localhost/foo_path')
+        self.assertIn('foobar', response.body)
+
+        response = self.app.get('http://localhost/bar_path/1234/56')
+        self.assertIn('foobar', response.body)
+
+    def test_callback_application(self):
+        app = self.decorator.callback_application()
+        self.assertEqual(
+            app.router.match_routes[0].handler.__name__,
+            'OAuth2Handler')
+
+    def test_required(self):
+        # An initial request to an oauth_required decorated path should be a
+        # redirect to start the OAuth dance.
+        self.assertEqual(self.decorator.flow, None)
+        self.assertEqual(self.decorator.credentials, None)
+        response = self.app.get('http://localhost/foo_path')
+        self.assertTrue(response.status.startswith('302'))
+        q = urllib.parse.parse_qs(
+            response.headers['Location'].split('?', 1)[1])
+        self.assertEqual('http://localhost/oauth2callback',
+                         q['redirect_uri'][0])
+        self.assertEqual('foo_client_id', q['client_id'][0])
+        self.assertEqual('foo_scope bar_scope', q['scope'][0])
+        self.assertEqual('http://localhost/foo_path',
+                         q['state'][0].rsplit(':', 1)[0])
+        self.assertEqual('code', q['response_type'][0])
+        self.assertEqual(False, self.decorator.has_credentials())
+
+        with mock.patch.object(appengine, '_parse_state_value',
+                               return_value='foo_path',
+                               autospec=True) as parse_state_value:
+            # Now simulate the callback to /oauth2callback.
+            response = self.app.get('/oauth2callback', {
+                'code': 'foo_access_code',
+                'state': 'foo_path:xsrfkey123',
+            })
+            parts = response.headers['Location'].split('?', 1)
+            self.assertEqual('http://localhost/foo_path', parts[0])
+            self.assertEqual(None, self.decorator.credentials)
+            if self.decorator._token_response_param:
+                response_query = urllib.parse.parse_qs(parts[1])
+                response = response_query[
+                    self.decorator._token_response_param][0]
+                self.assertEqual(Http2Mock.content,
+                                 json.loads(urllib.parse.unquote(response)))
+            self.assertEqual(self.decorator.flow, self.decorator._tls.flow)
+            self.assertEqual(self.decorator.credentials,
+                             self.decorator._tls.credentials)
+
+            parse_state_value.assert_called_once_with(
+                'foo_path:xsrfkey123', self.current_user)
+
+        # Now requesting the decorated path should work.
+        response = self.app.get('/foo_path')
+        self.assertEqual('200 OK', response.status)
+        self.assertEqual(True, self.had_credentials)
+        self.assertEqual('foo_refresh_token',
+                         self.found_credentials.refresh_token)
+        self.assertEqual('foo_access_token',
+                         self.found_credentials.access_token)
+        self.assertEqual(None, self.decorator.credentials)
+
+        # Raising an exception still clears the Credentials.
+        self.should_raise = Exception('')
+        with self.assertRaises(Exception):
+            self.app.get('/foo_path')
+        self.should_raise = False
+        self.assertEqual(None, self.decorator.credentials)
+
+        # Access token refresh error should start the dance again
+        self.should_raise = client.AccessTokenRefreshError()
+        response = self.app.get('/foo_path')
+        self.should_raise = False
+        self.assertTrue(response.status.startswith('302'))
+        query_params = urllib.parse.parse_qs(
+            response.headers['Location'].split('?', 1)[1])
+        self.assertEqual('http://localhost/oauth2callback',
+                         query_params['redirect_uri'][0])
+
+        # Invalidate the stored Credentials.
+        self.found_credentials.invalid = True
+        self.found_credentials.store.put(self.found_credentials)
+
+        # Invalid Credentials should start the OAuth dance again.
+        response = self.app.get('/foo_path')
+        self.assertTrue(response.status.startswith('302'))
+        query_params = urllib.parse.parse_qs(
+            response.headers['Location'].split('?', 1)[1])
+        self.assertEqual('http://localhost/oauth2callback',
+                         query_params['redirect_uri'][0])
+
+    def test_storage_delete(self):
+        # An initial request to an oauth_required decorated path should be a
+        # redirect to start the OAuth dance.
+        response = self.app.get('/foo_path')
+        self.assertTrue(response.status.startswith('302'))
+
+        with mock.patch.object(appengine, '_parse_state_value',
+                               return_value='foo_path',
+                               autospec=True) as parse_state_value:
+            # Now simulate the callback to /oauth2callback.
+            response = self.app.get('/oauth2callback', {
+                'code': 'foo_access_code',
+                'state': 'foo_path:xsrfkey123',
+            })
+            self.assertEqual('http://localhost/foo_path',
+                             response.headers['Location'])
+            self.assertEqual(None, self.decorator.credentials)
+
+            # Now requesting the decorated path should work.
+            response = self.app.get('/foo_path')
+
+            self.assertTrue(self.had_credentials)
+
+            # Credentials should be cleared after each call.
+            self.assertEqual(None, self.decorator.credentials)
+
+            # Invalidate the stored Credentials.
+            self.found_credentials.store.delete()
+
+            # Invalid Credentials should start the OAuth dance again.
+            response = self.app.get('/foo_path')
+            self.assertTrue(response.status.startswith('302'))
+
+            parse_state_value.assert_called_once_with(
+                'foo_path:xsrfkey123', self.current_user)
+
+    def test_aware(self):
+        # An initial request to an oauth_aware decorated path should
+        # not redirect.
+        response = self.app.get('http://localhost/bar_path/2012/01')
+        self.assertEqual('Hello World!', response.body)
+        self.assertEqual('200 OK', response.status)
+        self.assertEqual(False, self.decorator.has_credentials())
+        url = self.decorator.authorize_url()
+        q = urllib.parse.parse_qs(url.split('?', 1)[1])
+        self.assertEqual('http://localhost/oauth2callback',
+                         q['redirect_uri'][0])
+        self.assertEqual('foo_client_id', q['client_id'][0])
+        self.assertEqual('foo_scope bar_scope', q['scope'][0])
+        self.assertEqual('http://localhost/bar_path/2012/01',
+                         q['state'][0].rsplit(':', 1)[0])
+        self.assertEqual('code', q['response_type'][0])
+
+        with mock.patch.object(appengine, '_parse_state_value',
+                               return_value='bar_path',
+                               autospec=True) as parse_state_value:
+            # Now simulate the callback to /oauth2callback.
+            url = self.decorator.authorize_url()
+            response = self.app.get('/oauth2callback', {
+                'code': 'foo_access_code',
+                'state': 'bar_path:xsrfkey456',
+            })
+
+            self.assertEqual('http://localhost/bar_path',
+                             response.headers['Location'])
+            self.assertEqual(False, self.decorator.has_credentials())
+            parse_state_value.assert_called_once_with(
+                'bar_path:xsrfkey456', self.current_user)
+
+        # Now requesting the decorated path will have credentials.
+        response = self.app.get('/bar_path/2012/01')
+        self.assertEqual('200 OK', response.status)
+        self.assertEqual('Hello World!', response.body)
+        self.assertEqual(True, self.had_credentials)
+        self.assertEqual('foo_refresh_token',
+                         self.found_credentials.refresh_token)
+        self.assertEqual('foo_access_token',
+                         self.found_credentials.access_token)
+
+        # Credentials should be cleared after each call.
+        self.assertEqual(None, self.decorator.credentials)
+
+        # Raising an exception still clears the Credentials.
+        self.should_raise = Exception('')
+        with self.assertRaises(Exception):
+            self.app.get('/bar_path/2012/01')
+        self.should_raise = False
+        self.assertEqual(None, self.decorator.credentials)
+
+    def test_error_in_step2(self):
+        # An initial request to an oauth_aware decorated path should
+        # not redirect.
+        response = self.app.get('/bar_path/2012/01')
+        self.decorator.authorize_url()
+        response = self.app.get('/oauth2callback', {
+            'error': 'Bad<Stuff>Happened\''
+        })
+        self.assertEqual('200 OK', response.status)
+        self.assertTrue('Bad&lt;Stuff&gt;Happened&#39;' in response.body)
+
+    def test_kwargs_are_passed_to_underlying_flow(self):
+        decorator = appengine.OAuth2Decorator(
+            client_id='foo_client_id', client_secret='foo_client_secret',
+            user_agent='foo_user_agent', scope=['foo_scope', 'bar_scope'],
+            access_type='offline', prompt='consent',
+            revoke_uri='dummy_revoke_uri')
+        request_handler = MockRequestHandler()
+        decorator._create_flow(request_handler)
+
+        self.assertEqual('https://example.org/oauth2callback',
+                         decorator.flow.redirect_uri)
+        self.assertEqual('offline', decorator.flow.params['access_type'])
+        self.assertEqual('consent', decorator.flow.params['prompt'])
+        self.assertEqual('foo_user_agent', decorator.flow.user_agent)
+        self.assertEqual('dummy_revoke_uri', decorator.flow.revoke_uri)
+        self.assertEqual(None, decorator.flow.params.get('user_agent', None))
+        self.assertEqual(decorator.flow, decorator._tls.flow)
+
+    def test_token_response_param(self):
+        self.decorator._token_response_param = 'foobar'
+        self.test_required()
+
+    def test_decorator_from_client_secrets(self):
+        decorator = appengine.OAuth2DecoratorFromClientSecrets(
+            datafile('client_secrets.json'),
+            scope=['foo_scope', 'bar_scope'])
+        self._finish_setup(decorator, user_mock=UserMock)
+
+        self.assertFalse(decorator._in_error)
+        self.decorator = decorator
+        self.test_required()
+        http = self.decorator.http()
+        self.assertEquals('foo_access_token',
+                          http.request.credentials.access_token)
+
+        # revoke_uri is not required
+        self.assertEqual(self.decorator._revoke_uri,
+                         'https://accounts.google.com/o/oauth2/revoke')
+        self.assertEqual(self.decorator._revoke_uri,
+                         self.decorator.credentials.revoke_uri)
+
+    def test_decorator_from_client_secrets_toplevel(self):
+        decorator_patch = mock.patch(
+            'oauth2client.contrib.appengine.OAuth2DecoratorFromClientSecrets')
+
+        with decorator_patch as decorator_mock:
+            filename = datafile('client_secrets.json')
+            appengine.oauth2decorator_from_clientsecrets(
+                filename, scope='foo_scope')
+            decorator_mock.assert_called_once_with(
+                filename,
+                'foo_scope',
+                cache=None,
+                message=None)
+
+    def test_decorator_from_client_secrets_bad_type(self):
+        # NOTE: this code path is not currently reachable, as the only types
+        # that oauth2client.clientsecrets can load is web and installed, so
+        # this test forces execution of this code path. Despite not being
+        # normally reachable, this should remain in case future types of
+        # credentials are added.
+
+        loadfile_patch = mock.patch(
+            'oauth2client.contrib.appengine.clientsecrets.loadfile')
+        with loadfile_patch as loadfile_mock:
+            loadfile_mock.return_value = ('badtype', None)
+            with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+                appengine.OAuth2DecoratorFromClientSecrets(
+                    'doesntmatter.json',
+                    scope=['foo_scope', 'bar_scope'])
+
+    def test_decorator_from_client_secrets_kwargs(self):
+        decorator = appengine.OAuth2DecoratorFromClientSecrets(
+            datafile('client_secrets.json'),
+            scope=['foo_scope', 'bar_scope'],
+            prompt='consent')
+        self.assertIn('prompt', decorator._kwargs)
+
+    def test_decorator_from_cached_client_secrets(self):
+        cache_mock = CacheMock()
+        load_and_cache('client_secrets.json', 'secret', cache_mock)
+        decorator = appengine.OAuth2DecoratorFromClientSecrets(
+            # filename, scope, message=None, cache=None
+            'secret', '', cache=cache_mock)
+        self.assertFalse(decorator._in_error)
+
+    def test_decorator_from_client_secrets_not_logged_in_required(self):
+        decorator = appengine.OAuth2DecoratorFromClientSecrets(
+            datafile('client_secrets.json'),
+            scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage')
+        self.decorator = decorator
+        self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
+
+        self.assertFalse(decorator._in_error)
+
+        # An initial request to an oauth_required decorated path should be a
+        # redirect to login.
+        response = self.app.get('/foo_path')
+        self.assertTrue(response.status.startswith('302'))
+        self.assertTrue('Login' in str(response))
+
+    def test_decorator_from_client_secrets_not_logged_in_aware(self):
+        decorator = appengine.OAuth2DecoratorFromClientSecrets(
+            datafile('client_secrets.json'),
+            scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage')
+        self.decorator = decorator
+        self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
+
+        # An initial request to an oauth_aware decorated path should be a
+        # redirect to login.
+        response = self.app.get('/bar_path/2012/03')
+        self.assertTrue(response.status.startswith('302'))
+        self.assertTrue('Login' in str(response))
+
+    def test_decorator_from_unfilled_client_secrets_required(self):
+        MESSAGE = 'File is missing'
+        try:
+            appengine.OAuth2DecoratorFromClientSecrets(
+                datafile('unfilled_client_secrets.json'),
+                scope=['foo_scope', 'bar_scope'], message=MESSAGE)
+        except clientsecrets.InvalidClientSecretsError:
+            pass
+
+    def test_decorator_from_unfilled_client_secrets_aware(self):
+        MESSAGE = 'File is missing'
+        try:
+            appengine.OAuth2DecoratorFromClientSecrets(
+                datafile('unfilled_client_secrets.json'),
+                scope=['foo_scope', 'bar_scope'], message=MESSAGE)
+        except clientsecrets.InvalidClientSecretsError:
+            pass
+
+    def test_decorator_from_client_secrets_with_optional_settings(self):
+        # Test that the decorator works with the absense of a revoke_uri in
+        # the client secrets.
+        loadfile_patch = mock.patch(
+            'oauth2client.contrib.appengine.clientsecrets.loadfile')
+        with loadfile_patch as loadfile_mock:
+            loadfile_mock.return_value = (clientsecrets.TYPE_WEB, {
+                "client_id": "foo_client_id",
+                "client_secret": "foo_client_secret",
+                "redirect_uris": [],
+                "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
+                "token_uri": "https://www.googleapis.com/oauth2/v4/token",
+                # No revoke URI
+            })
+
+            decorator = appengine.OAuth2DecoratorFromClientSecrets(
+                'doesntmatter.json',
+                scope=['foo_scope', 'bar_scope'])
+
+        self.assertEqual(decorator._revoke_uri, oauth2client.GOOGLE_REVOKE_URI)
+        # This is never set, but it's consistent with other tests.
+        self.assertFalse(decorator._in_error)
+
+    def test_invalid_state(self):
+        with mock.patch.object(appengine, '_parse_state_value',
+                               return_value=None, autospec=True):
+            # Now simulate the callback to /oauth2callback.
+            response = self.app.get('/oauth2callback', {
+                'code': 'foo_access_code',
+                'state': 'foo_path:xsrfkey123',
+            })
+            self.assertEqual('200 OK', response.status)
+            self.assertEqual('The authorization request failed', response.body)
+
+
+class DecoratorXsrfSecretTests(unittest2.TestCase):
+    """Test xsrf_secret_key."""
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_build_and_parse_state(self):
+        secret = appengine.xsrf_secret_key()
+
+        # Secret shouldn't change from call to call.
+        secret2 = appengine.xsrf_secret_key()
+        self.assertEqual(secret, secret2)
+
+        # Secret shouldn't change if memcache goes away.
+        memcache.delete(appengine.XSRF_MEMCACHE_ID,
+                        namespace=appengine.OAUTH2CLIENT_NAMESPACE)
+        secret3 = appengine.xsrf_secret_key()
+        self.assertEqual(secret2, secret3)
+
+        # Secret should change if both memcache and the model goes away.
+        memcache.delete(appengine.XSRF_MEMCACHE_ID,
+                        namespace=appengine.OAUTH2CLIENT_NAMESPACE)
+        model = appengine.SiteXsrfSecretKey.get_or_insert('site')
+        model.delete()
+
+        secret4 = appengine.xsrf_secret_key()
+        self.assertNotEqual(secret3, secret4)
+
+    def test_ndb_insert_db_get(self):
+        secret = appengine._generate_new_xsrf_secret_key()
+        appengine.SiteXsrfSecretKeyNDB(id='site', secret=secret).put()
+
+        site_key = appengine.SiteXsrfSecretKey.get_by_key_name('site')
+        self.assertEqual(site_key.secret, secret)
+
+    def test_db_insert_ndb_get(self):
+        secret = appengine._generate_new_xsrf_secret_key()
+        appengine.SiteXsrfSecretKey(key_name='site', secret=secret).put()
+
+        site_key = appengine.SiteXsrfSecretKeyNDB.get_by_id('site')
+        self.assertEqual(site_key.secret, secret)
+
+
+class DecoratorXsrfProtectionTests(unittest2.TestCase):
+    """Test _build_state_value and _parse_state_value."""
+
+    def setUp(self):
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+
+    def tearDown(self):
+        self.testbed.deactivate()
+
+    def test_build_and_parse_state(self):
+        state = appengine._build_state_value(MockRequestHandler(), UserMock())
+        self.assertEqual(
+            'https://example.org',
+            appengine._parse_state_value(state, UserMock()))
+        redirect_uri = appengine._parse_state_value(state[1:], UserMock())
+        self.assertIsNone(redirect_uri)
diff --git a/tests/contrib/test_devshell.py b/tests/contrib/test_devshell.py
new file mode 100644
index 0000000..659a53b
--- /dev/null
+++ b/tests/contrib/test_devshell.py
@@ -0,0 +1,266 @@
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Tests for oauth2client.contrib.devshell."""
+
+import datetime
+import json
+import os
+import socket
+import threading
+
+import mock
+import unittest2
+
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client.contrib import devshell
+
+# A dummy value to use for the expires_in field
+# in CredentialInfoResponse.
+EXPIRES_IN = 1000
+DEFAULT_CREDENTIAL_JSON = json.dumps([
+    'joe@example.com',
+    'fooproj',
+    'sometoken',
+    EXPIRES_IN
+])
+
+
+class TestCredentialInfoResponse(unittest2.TestCase):
+
+    def test_constructor_with_non_list(self):
+        json_non_list = '{}'
+        with self.assertRaises(ValueError):
+            devshell.CredentialInfoResponse(json_non_list)
+
+    def test_constructor_with_bad_json(self):
+        json_non_list = '{BADJSON'
+        with self.assertRaises(ValueError):
+            devshell.CredentialInfoResponse(json_non_list)
+
+    def test_constructor_empty_list(self):
+        info_response = devshell.CredentialInfoResponse('[]')
+        self.assertEqual(info_response.user_email, None)
+        self.assertEqual(info_response.project_id, None)
+        self.assertEqual(info_response.access_token, None)
+        self.assertEqual(info_response.expires_in, None)
+
+    def test_constructor_full_list(self):
+        user_email = 'user_email'
+        project_id = 'project_id'
+        access_token = 'access_token'
+        expires_in = 1
+        json_string = json.dumps(
+            [user_email, project_id, access_token, expires_in])
+        info_response = devshell.CredentialInfoResponse(json_string)
+        self.assertEqual(info_response.user_email, user_email)
+        self.assertEqual(info_response.project_id, project_id)
+        self.assertEqual(info_response.access_token, access_token)
+        self.assertEqual(info_response.expires_in, expires_in)
+
+
+class Test_SendRecv(unittest2.TestCase):
+
+    def test_port_zero(self):
+        with mock.patch('oauth2client.contrib.devshell.os') as os_mod:
+            os_mod.getenv = mock.MagicMock(name='getenv', return_value=0)
+            with self.assertRaises(devshell.NoDevshellServer):
+                devshell._SendRecv()
+            os_mod.getenv.assert_called_once_with(devshell.DEVSHELL_ENV, 0)
+
+    def test_no_newline_in_received_header(self):
+        non_zero_port = 1
+        sock = mock.MagicMock()
+
+        header_without_newline = ''
+        sock.recv(6).decode = mock.MagicMock(
+            name='decode', return_value=header_without_newline)
+
+        with mock.patch('oauth2client.contrib.devshell.os') as os_mod:
+            os_mod.getenv = mock.MagicMock(name='getenv',
+                                           return_value=non_zero_port)
+            with mock.patch('oauth2client.contrib.devshell.socket') as socket:
+                socket.socket = mock.MagicMock(name='socket',
+                                               return_value=sock)
+                with self.assertRaises(devshell.CommunicationError):
+                    devshell._SendRecv()
+                os_mod.getenv.assert_called_once_with(devshell.DEVSHELL_ENV, 0)
+                socket.socket.assert_called_once_with()
+                sock.recv(6).decode.assert_called_once_with()
+
+                data = devshell.CREDENTIAL_INFO_REQUEST_JSON
+                msg = _helpers._to_bytes(
+                    '{0}\n{1}'.format(len(data), data), encoding='utf-8')
+                expected_sock_calls = [
+                    mock.call.recv(6),  # From the set-up above
+                    mock.call.connect(('localhost', non_zero_port)),
+                    mock.call.sendall(msg),
+                    mock.call.recv(6),
+                    mock.call.recv(6),  # From the check above
+                ]
+                self.assertEqual(sock.method_calls, expected_sock_calls)
+
+
+class _AuthReferenceServer(threading.Thread):
+
+    def __init__(self, response=None):
+        super(_AuthReferenceServer, self).__init__(None)
+        self.response = response or DEFAULT_CREDENTIAL_JSON
+        self.bad_request = False
+
+    def __enter__(self):
+        return self.start_server()
+
+    def start_server(self):
+        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._socket.bind(('localhost', 0))
+        port = self._socket.getsockname()[1]
+        os.environ[devshell.DEVSHELL_ENV] = str(port)
+        self._socket.listen(0)
+        self.daemon = True
+        self.start()
+        return self
+
+    def __exit__(self, e_type, value, traceback):
+        self.stop_server()
+
+    def stop_server(self):
+        del os.environ[devshell.DEVSHELL_ENV]
+        self._socket.close()
+
+    def run(self):
+        s = None
+        try:
+            # Do not set the timeout on the socket, leave it in the blocking
+            # mode as setting the timeout seems to cause spurious EAGAIN
+            # errors on OSX.
+            self._socket.settimeout(None)
+
+            s, unused_addr = self._socket.accept()
+            resp_buffer = ''
+            resp_1 = s.recv(6).decode()
+            nstr, extra = resp_1.split('\n', 1)
+            resp_buffer = extra
+            n = int(nstr)
+            to_read = n - len(extra)
+            if to_read > 0:
+                resp_buffer += _helpers._from_bytes(
+                    s.recv(to_read, socket.MSG_WAITALL))
+            if resp_buffer != devshell.CREDENTIAL_INFO_REQUEST_JSON:
+                self.bad_request = True
+            l = len(self.response)
+            s.sendall('{0}\n{1}'.format(l, self.response).encode())
+        finally:
+            # Will fail if s is None, but these tests never encounter
+            # that scenario.
+            s.close()
+
+
+class DevshellCredentialsTests(unittest2.TestCase):
+
+    def test_signals_no_server(self):
+        with self.assertRaises(devshell.NoDevshellServer):
+            devshell.DevshellCredentials()
+
+    def test_bad_message_to_mock_server(self):
+        request_content = devshell.CREDENTIAL_INFO_REQUEST_JSON + 'extrastuff'
+        request_message = _helpers._to_bytes(
+            '{0}\n{1}'.format(len(request_content), request_content))
+        response_message = 'foobar'
+        with _AuthReferenceServer(response_message) as auth_server:
+            self.assertFalse(auth_server.bad_request)
+            sock = socket.socket()
+            port = int(os.getenv(devshell.DEVSHELL_ENV, 0))
+            sock.connect(('localhost', port))
+            sock.sendall(request_message)
+
+            # Mimic the receive part of _SendRecv
+            header = sock.recv(6).decode()
+            len_str, result = header.split('\n', 1)
+            to_read = int(len_str) - len(result)
+            result += sock.recv(to_read, socket.MSG_WAITALL).decode()
+
+        self.assertTrue(auth_server.bad_request)
+        self.assertEqual(result, response_message)
+
+    def test_request_response(self):
+        with _AuthReferenceServer():
+            response = devshell._SendRecv()
+            self.assertEqual(response.user_email, 'joe@example.com')
+            self.assertEqual(response.project_id, 'fooproj')
+            self.assertEqual(response.access_token, 'sometoken')
+
+    def test_no_refresh_token(self):
+        with _AuthReferenceServer():
+            creds = devshell.DevshellCredentials()
+            self.assertEquals(None, creds.refresh_token)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_reads_credentials(self, utcnow):
+        NOW = datetime.datetime(1992, 12, 31)
+        utcnow.return_value = NOW
+        with _AuthReferenceServer():
+            creds = devshell.DevshellCredentials()
+            self.assertEqual('joe@example.com', creds.user_email)
+            self.assertEqual('fooproj', creds.project_id)
+            self.assertEqual('sometoken', creds.access_token)
+            self.assertEqual(
+                NOW + datetime.timedelta(seconds=EXPIRES_IN),
+                creds.token_expiry)
+            utcnow.assert_called_once_with()
+
+    def test_handles_skipped_fields(self):
+        with _AuthReferenceServer('["joe@example.com"]'):
+            creds = devshell.DevshellCredentials()
+            self.assertEqual('joe@example.com', creds.user_email)
+            self.assertEqual(None, creds.project_id)
+            self.assertEqual(None, creds.access_token)
+            self.assertEqual(None, creds.token_expiry)
+
+    def test_handles_tiny_response(self):
+        with _AuthReferenceServer('[]'):
+            creds = devshell.DevshellCredentials()
+            self.assertEqual(None, creds.user_email)
+            self.assertEqual(None, creds.project_id)
+            self.assertEqual(None, creds.access_token)
+
+    def test_handles_ignores_extra_fields(self):
+        with _AuthReferenceServer(
+                '["joe@example.com", "fooproj", "sometoken", 1, "extra"]'):
+            creds = devshell.DevshellCredentials()
+            self.assertEqual('joe@example.com', creds.user_email)
+            self.assertEqual('fooproj', creds.project_id)
+            self.assertEqual('sometoken', creds.access_token)
+
+    def test_refuses_to_save_to_well_known_file(self):
+        ORIGINAL_ISDIR = os.path.isdir
+        try:
+            os.path.isdir = lambda path: True
+            with _AuthReferenceServer():
+                creds = devshell.DevshellCredentials()
+                with self.assertRaises(NotImplementedError):
+                    client.save_to_well_known_file(creds)
+        finally:
+            os.path.isdir = ORIGINAL_ISDIR
+
+    def test_from_json(self):
+        with self.assertRaises(NotImplementedError):
+            devshell.DevshellCredentials.from_json(None)
+
+    def test_serialization_data(self):
+        with _AuthReferenceServer('[]'):
+            credentials = devshell.DevshellCredentials()
+            with self.assertRaises(NotImplementedError):
+                getattr(credentials, 'serialization_data')
diff --git a/tests/contrib/test_dictionary_storage.py b/tests/contrib/test_dictionary_storage.py
new file mode 100644
index 0000000..888c938
--- /dev/null
+++ b/tests/contrib/test_dictionary_storage.py
@@ -0,0 +1,107 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for oauth2client.contrib.dictionary_storage"""
+
+import unittest2
+
+import oauth2client
+from oauth2client import client
+from oauth2client.contrib import dictionary_storage
+
+
+def _generate_credentials(scopes=None):
+    return client.OAuth2Credentials(
+        'access_tokenz',
+        'client_idz',
+        'client_secretz',
+        'refresh_tokenz',
+        '3600',
+        oauth2client.GOOGLE_TOKEN_URI,
+        'Test',
+        id_token={
+            'sub': '123',
+            'email': 'user@example.com'
+        },
+        scopes=scopes)
+
+
+class DictionaryStorageTests(unittest2.TestCase):
+
+    def test_constructor_defaults(self):
+        dictionary = {}
+        key = 'test-key'
+        storage = dictionary_storage.DictionaryStorage(dictionary, key)
+
+        self.assertEqual(dictionary, storage._dictionary)
+        self.assertEqual(key, storage._key)
+        self.assertIsNone(storage._lock)
+
+    def test_constructor_explicit(self):
+        dictionary = {}
+        key = 'test-key'
+        storage = dictionary_storage.DictionaryStorage(dictionary, key)
+
+        lock = object()
+        storage = dictionary_storage.DictionaryStorage(
+            dictionary, key, lock=lock)
+        self.assertEqual(storage._lock, lock)
+
+    def test_get(self):
+        credentials = _generate_credentials()
+        dictionary = {}
+        key = 'credentials'
+        storage = dictionary_storage.DictionaryStorage(dictionary, key)
+
+        self.assertIsNone(storage.get())
+
+        dictionary[key] = credentials.to_json()
+        returned = storage.get()
+
+        self.assertIsNotNone(returned)
+        self.assertEqual(returned.access_token, credentials.access_token)
+        self.assertEqual(returned.id_token, credentials.id_token)
+        self.assertEqual(returned.refresh_token, credentials.refresh_token)
+        self.assertEqual(returned.client_id, credentials.client_id)
+
+    def test_put(self):
+        credentials = _generate_credentials()
+        dictionary = {}
+        key = 'credentials'
+        storage = dictionary_storage.DictionaryStorage(dictionary, key)
+
+        storage.put(credentials)
+        returned = storage.get()
+
+        self.assertIn(key, dictionary)
+        self.assertIsNotNone(returned)
+        self.assertEqual(returned.access_token, credentials.access_token)
+        self.assertEqual(returned.id_token, credentials.id_token)
+        self.assertEqual(returned.refresh_token, credentials.refresh_token)
+        self.assertEqual(returned.client_id, credentials.client_id)
+
+    def test_delete(self):
+        credentials = _generate_credentials()
+        dictionary = {}
+        key = 'credentials'
+        storage = dictionary_storage.DictionaryStorage(dictionary, key)
+
+        storage.put(credentials)
+
+        self.assertIn(key, dictionary)
+
+        storage.delete()
+
+        self.assertNotIn(key, dictionary)
+        self.assertIsNone(storage.get())
diff --git a/tests/contrib/test_flask_util.py b/tests/contrib/test_flask_util.py
new file mode 100644
index 0000000..74cb218
--- /dev/null
+++ b/tests/contrib/test_flask_util.py
@@ -0,0 +1,531 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for the Flask utilities"""
+
+import datetime
+import json
+import logging
+
+import flask
+import httplib2
+import mock
+import six.moves.http_client as httplib
+import six.moves.urllib.parse as urlparse
+import unittest2
+
+import oauth2client
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client.contrib import flask_util
+
+
+__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
+
+
+class Http2Mock(object):
+    """Mock httplib2.Http for code exchange / refresh"""
+
+    def __init__(self, status=httplib.OK, **kwargs):
+        self.status = status
+        self.content = {
+            'access_token': 'foo_access_token',
+            'refresh_token': 'foo_refresh_token',
+            'expires_in': 3600,
+            'extra': 'value',
+        }
+        self.content.update(kwargs)
+
+    def request(self, token_uri, method, body, headers, *args, **kwargs):
+        self.body = body
+        self.headers = headers
+        return (self, json.dumps(self.content).encode('utf-8'))
+
+    def __enter__(self):
+        self.httplib2_orig = httplib2.Http
+        httplib2.Http = self
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        httplib2.Http = self.httplib2_orig
+
+    def __call__(self, *args, **kwargs):
+        return self
+
+
+class FlaskOAuth2Tests(unittest2.TestCase):
+
+    def setUp(self):
+        self.app = flask.Flask(__name__)
+        self.app.testing = True
+        self.app.config['SECRET_KEY'] = 'notasecert'
+        self.app.logger.setLevel(logging.CRITICAL)
+        self.oauth2 = flask_util.UserOAuth2(
+            self.app,
+            client_id='client_idz',
+            client_secret='client_secretz')
+
+    def _generate_credentials(self, scopes=None):
+        return client.OAuth2Credentials(
+            'access_tokenz',
+            'client_idz',
+            'client_secretz',
+            'refresh_tokenz',
+            datetime.datetime.utcnow() + datetime.timedelta(seconds=3600),
+            oauth2client.GOOGLE_TOKEN_URI,
+            'Test',
+            id_token={
+                'sub': '123',
+                'email': 'user@example.com'
+            },
+            scopes=scopes)
+
+    def test_explicit_configuration(self):
+        oauth2 = flask_util.UserOAuth2(
+            flask.Flask(__name__), client_id='id', client_secret='secret')
+
+        self.assertEqual(oauth2.client_id, 'id')
+        self.assertEqual(oauth2.client_secret, 'secret')
+
+        return_val = (
+            clientsecrets.TYPE_WEB,
+            {'client_id': 'id', 'client_secret': 'secret'})
+
+        with mock.patch('oauth2client.clientsecrets.loadfile',
+                        return_value=return_val):
+
+            oauth2 = flask_util.UserOAuth2(
+                flask.Flask(__name__), client_secrets_file='file.json')
+
+            self.assertEqual(oauth2.client_id, 'id')
+            self.assertEqual(oauth2.client_secret, 'secret')
+
+    def test_delayed_configuration(self):
+        app = flask.Flask(__name__)
+        oauth2 = flask_util.UserOAuth2()
+        oauth2.init_app(app, client_id='id', client_secret='secret')
+        self.assertEqual(oauth2.app, app)
+
+    def test_explicit_storage(self):
+        storage_mock = mock.Mock()
+        oauth2 = flask_util.UserOAuth2(
+            flask.Flask(__name__), storage=storage_mock, client_id='id',
+            client_secret='secret')
+        self.assertEqual(oauth2.storage, storage_mock)
+
+    def test_explicit_scopes(self):
+        oauth2 = flask_util.UserOAuth2(
+            flask.Flask(__name__), scopes=['1', '2'], client_id='id',
+            client_secret='secret')
+        self.assertEqual(oauth2.scopes, ['1', '2'])
+
+    def test_bad_client_secrets(self):
+        return_val = (
+            'other',
+            {'client_id': 'id', 'client_secret': 'secret'})
+
+        with mock.patch('oauth2client.clientsecrets.loadfile',
+                        return_value=return_val):
+            with self.assertRaises(ValueError):
+                flask_util.UserOAuth2(flask.Flask(__name__),
+                                      client_secrets_file='file.json')
+
+    def test_app_configuration(self):
+        app = flask.Flask(__name__)
+        app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'id'
+        app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'secret'
+
+        oauth2 = flask_util.UserOAuth2(app)
+
+        self.assertEqual(oauth2.client_id, 'id')
+        self.assertEqual(oauth2.client_secret, 'secret')
+
+        return_val = (
+            clientsecrets.TYPE_WEB,
+            {'client_id': 'id2', 'client_secret': 'secret2'})
+
+        with mock.patch('oauth2client.clientsecrets.loadfile',
+                        return_value=return_val):
+
+            app = flask.Flask(__name__)
+            app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'file.json'
+            oauth2 = flask_util.UserOAuth2(app)
+
+            self.assertEqual(oauth2.client_id, 'id2')
+            self.assertEqual(oauth2.client_secret, 'secret2')
+
+    def test_no_configuration(self):
+        with self.assertRaises(ValueError):
+            flask_util.UserOAuth2(flask.Flask(__name__))
+
+    def test_create_flow(self):
+        with self.app.test_request_context():
+            flow = self.oauth2._make_flow()
+            state = json.loads(flow.params['state'])
+            self.assertIn('google_oauth2_csrf_token', flask.session)
+            self.assertEqual(
+                flask.session['google_oauth2_csrf_token'], state['csrf_token'])
+            self.assertEqual(flow.client_id, self.oauth2.client_id)
+            self.assertEqual(flow.client_secret, self.oauth2.client_secret)
+            self.assertIn('http', flow.redirect_uri)
+            self.assertIn('oauth2callback', flow.redirect_uri)
+
+            flow = self.oauth2._make_flow(return_url='/return_url')
+            state = json.loads(flow.params['state'])
+            self.assertEqual(state['return_url'], '/return_url')
+
+            flow = self.oauth2._make_flow(extra_arg='test')
+            self.assertEqual(flow.params['extra_arg'], 'test')
+
+        # Test extra args specified in the constructor.
+        app = flask.Flask(__name__)
+        app.config['SECRET_KEY'] = 'notasecert'
+        oauth2 = flask_util.UserOAuth2(
+            app, client_id='client_id', client_secret='secret',
+            extra_arg='test')
+
+        with app.test_request_context():
+            flow = oauth2._make_flow()
+            self.assertEqual(flow.params['extra_arg'], 'test')
+
+    def test_authorize_view(self):
+        with self.app.test_client() as client:
+            response = client.get('/oauth2authorize')
+            location = response.headers['Location']
+            q = urlparse.parse_qs(location.split('?', 1)[1])
+            state = json.loads(q['state'][0])
+
+            self.assertIn(oauth2client.GOOGLE_AUTH_URI, location)
+            self.assertNotIn(self.oauth2.client_secret, location)
+            self.assertIn(self.oauth2.client_id, q['client_id'])
+            self.assertEqual(
+                flask.session['google_oauth2_csrf_token'], state['csrf_token'])
+            self.assertEqual(state['return_url'], '/')
+
+        with self.app.test_client() as client:
+            response = client.get('/oauth2authorize?return_url=/test')
+            location = response.headers['Location']
+            q = urlparse.parse_qs(location.split('?', 1)[1])
+            state = json.loads(q['state'][0])
+            self.assertEqual(state['return_url'], '/test')
+
+        with self.app.test_client() as client:
+            response = client.get('/oauth2authorize?extra_param=test')
+            location = response.headers['Location']
+            self.assertIn('extra_param=test', location)
+
+    def _setup_callback_state(self, client, **kwargs):
+        with self.app.test_request_context():
+            # Flask doesn't create a request context with a session
+            # transaction for some reason, so, set up the flow here,
+            # then apply it to the session in the transaction.
+            if not kwargs:
+                self.oauth2._make_flow(return_url='/return_url')
+            else:
+                self.oauth2._make_flow(**kwargs)
+
+            with client.session_transaction() as session:
+                session.update(flask.session)
+                csrf_token = session['google_oauth2_csrf_token']
+                flow = flask_util._get_flow_for_token(csrf_token)
+                state = flow.params['state']
+
+        return state
+
+    def test_callback_view(self):
+        self.oauth2.storage = mock.Mock()
+        with self.app.test_client() as client:
+            with Http2Mock() as http:
+                state = self._setup_callback_state(client)
+
+                response = client.get(
+                    '/oauth2callback?state={0}&code=codez'.format(state))
+
+                self.assertEqual(response.status_code, httplib.FOUND)
+                self.assertIn('/return_url', response.headers['Location'])
+                self.assertIn(self.oauth2.client_secret, http.body)
+                self.assertIn('codez', http.body)
+                self.assertTrue(self.oauth2.storage.put.called)
+
+    def test_authorize_callback(self):
+        self.oauth2.authorize_callback = mock.Mock()
+        self.test_callback_view()
+        self.assertTrue(self.oauth2.authorize_callback.called)
+
+    def test_callback_view_errors(self):
+        # Error supplied to callback
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_csrf_token'] = 'tokenz'
+
+            response = client.get('/oauth2callback?state={}&error=something')
+            self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+            self.assertIn('something', response.data.decode('utf-8'))
+
+        # CSRF mismatch
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_csrf_token'] = 'goodstate'
+
+            state = json.dumps({
+                'csrf_token': 'badstate',
+                'return_url': '/return_url'
+            })
+
+            response = client.get(
+                '/oauth2callback?state={0}&code=codez'.format(state))
+            self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+
+        # KeyError, no CSRF state.
+        with self.app.test_client() as client:
+            response = client.get('/oauth2callback?state={}&code=codez')
+            self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+
+        # Code exchange error
+        with self.app.test_client() as client:
+            state = self._setup_callback_state(client)
+
+            with Http2Mock(status=httplib.INTERNAL_SERVER_ERROR):
+                response = client.get(
+                    '/oauth2callback?state={0}&code=codez'.format(state))
+                self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+
+        # Invalid state json
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_csrf_token'] = 'tokenz'
+
+            state = '[{'
+            response = client.get(
+                '/oauth2callback?state={0}&code=codez'.format(state))
+            self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+
+        # Missing flow.
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_csrf_token'] = 'tokenz'
+
+            state = json.dumps({
+                'csrf_token': 'tokenz',
+                'return_url': '/return_url'
+            })
+
+            response = client.get(
+                '/oauth2callback?state={0}&code=codez'.format(state))
+            self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+
+    def test_no_credentials(self):
+        with self.app.test_request_context():
+            self.assertFalse(self.oauth2.has_credentials())
+            self.assertTrue(self.oauth2.credentials is None)
+            self.assertTrue(self.oauth2.user_id is None)
+            self.assertTrue(self.oauth2.email is None)
+            with self.assertRaises(ValueError):
+                self.oauth2.http()
+            self.assertFalse(self.oauth2.storage.get())
+            self.oauth2.storage.delete()
+
+    def test_with_credentials(self):
+        credentials = self._generate_credentials()
+        with self.app.test_request_context():
+            self.oauth2.storage.put(credentials)
+            self.assertEqual(
+                self.oauth2.credentials.access_token, credentials.access_token)
+            self.assertEqual(
+                self.oauth2.credentials.refresh_token,
+                credentials.refresh_token)
+            self.assertEqual(self.oauth2.user_id, '123')
+            self.assertEqual(self.oauth2.email, 'user@example.com')
+            self.assertTrue(self.oauth2.http())
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_with_expired_credentials(self, utcnow):
+        utcnow.return_value = datetime.datetime(1990, 5, 29)
+
+        credentials = self._generate_credentials()
+        credentials.token_expiry = datetime.datetime(1990, 5, 28)
+
+        # Has a refresh token, so this should be fine.
+        with self.app.test_request_context():
+            self.oauth2.storage.put(credentials)
+            self.assertTrue(self.oauth2.has_credentials())
+
+        # Without a refresh token this should return false.
+        credentials.refresh_token = None
+        with self.app.test_request_context():
+            self.oauth2.storage.put(credentials)
+            self.assertFalse(self.oauth2.has_credentials())
+
+    def test_bad_id_token(self):
+        credentials = self._generate_credentials()
+        credentials.id_token = {}
+        with self.app.test_request_context():
+            self.oauth2.storage.put(credentials)
+            self.assertTrue(self.oauth2.user_id is None)
+            self.assertTrue(self.oauth2.email is None)
+
+    def test_required(self):
+        @self.app.route('/protected')
+        @self.oauth2.required
+        def index():
+            return 'Hello'
+
+        # No credentials, should redirect
+        with self.app.test_client() as client:
+            response = client.get('/protected')
+            self.assertEqual(response.status_code, httplib.FOUND)
+            self.assertIn('oauth2authorize', response.headers['Location'])
+            self.assertIn('protected', response.headers['Location'])
+
+        credentials = self._generate_credentials(scopes=self.oauth2.scopes)
+
+        # With credentials, should allow
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_credentials'] = credentials.to_json()
+
+            response = client.get('/protected')
+            self.assertEqual(response.status_code, httplib.OK)
+            self.assertIn('Hello', response.data.decode('utf-8'))
+
+        # Expired credentials with refresh token, should allow.
+        credentials.token_expiry = datetime.datetime(1990, 5, 28)
+        with mock.patch('oauth2client.client._UTCNOW') as utcnow:
+            utcnow.return_value = datetime.datetime(1990, 5, 29)
+
+            with self.app.test_client() as client:
+                with client.session_transaction() as session:
+                    session['google_oauth2_credentials'] = (
+                        credentials.to_json())
+
+                response = client.get('/protected')
+                self.assertEqual(response.status_code, httplib.OK)
+                self.assertIn('Hello', response.data.decode('utf-8'))
+
+        # Expired credentials without a refresh token, should redirect.
+        credentials.refresh_token = None
+        with mock.patch('oauth2client.client._UTCNOW') as utcnow:
+            utcnow.return_value = datetime.datetime(1990, 5, 29)
+
+            with self.app.test_client() as client:
+                with client.session_transaction() as session:
+                    session['google_oauth2_credentials'] = (
+                        credentials.to_json())
+
+                response = client.get('/protected')
+            self.assertEqual(response.status_code, httplib.FOUND)
+            self.assertIn('oauth2authorize', response.headers['Location'])
+            self.assertIn('protected', response.headers['Location'])
+
+    def _create_incremental_auth_app(self):
+        self.app = flask.Flask(__name__)
+        self.app.testing = True
+        self.app.config['SECRET_KEY'] = 'notasecert'
+        self.oauth2 = flask_util.UserOAuth2(
+            self.app,
+            client_id='client_idz',
+            client_secret='client_secretz',
+            include_granted_scopes=True)
+
+        @self.app.route('/one')
+        @self.oauth2.required(scopes=['one'])
+        def one():
+            return 'Hello'
+
+        @self.app.route('/two')
+        @self.oauth2.required(scopes=['two', 'three'])
+        def two():
+            return 'Hello'
+
+    def test_incremental_auth(self):
+        self._create_incremental_auth_app()
+
+        # No credentials, should redirect
+        with self.app.test_client() as client:
+            response = client.get('/one')
+            self.assertIn('one', response.headers['Location'])
+            self.assertEqual(response.status_code, httplib.FOUND)
+
+        # Credentials for one. /one should allow, /two should redirect.
+        credentials = self._generate_credentials(scopes=['email', 'one'])
+
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_credentials'] = credentials.to_json()
+
+            response = client.get('/one')
+            self.assertEqual(response.status_code, httplib.OK)
+
+            response = client.get('/two')
+            self.assertIn('two', response.headers['Location'])
+            self.assertEqual(response.status_code, httplib.FOUND)
+
+            # Starting the authorization flow should include the
+            # include_granted_scopes parameter as well as the scopes.
+            response = client.get(response.headers['Location'][17:])
+            q = urlparse.parse_qs(
+                response.headers['Location'].split('?', 1)[1])
+            self.assertIn('include_granted_scopes', q)
+            self.assertEqual(
+                set(q['scope'][0].split(' ')),
+                set(['one', 'email', 'two', 'three']))
+
+        # Actually call two() without a redirect.
+        credentials2 = self._generate_credentials(
+            scopes=['email', 'two', 'three'])
+
+        with self.app.test_client() as client:
+            with client.session_transaction() as session:
+                session['google_oauth2_credentials'] = credentials2.to_json()
+
+            response = client.get('/two')
+            self.assertEqual(response.status_code, httplib.OK)
+
+    def test_incremental_auth_exchange(self):
+        self._create_incremental_auth_app()
+
+        with Http2Mock():
+            with self.app.test_client() as client:
+                state = self._setup_callback_state(
+                    client,
+                    return_url='/return_url',
+                    # Incremental auth scopes.
+                    scopes=['one', 'two'])
+
+                response = client.get(
+                    '/oauth2callback?state={0}&code=codez'.format(state))
+                self.assertEqual(response.status_code, httplib.FOUND)
+
+                credentials = self.oauth2.credentials
+                self.assertTrue(
+                    credentials.has_scopes(['email', 'one', 'two']))
+
+    def test_refresh(self):
+        with self.app.test_request_context():
+            with mock.patch('flask.session'):
+                self.oauth2.storage.put(self._generate_credentials())
+
+                self.oauth2.credentials.refresh(
+                    Http2Mock(access_token='new_token'))
+
+                self.assertEqual(
+                    self.oauth2.storage.get().access_token, 'new_token')
+
+    def test_delete(self):
+        with self.app.test_request_context():
+
+            self.oauth2.storage.put(self._generate_credentials())
+            self.oauth2.storage.delete()
+
+            self.assertNotIn('google_oauth2_credentials', flask.session)
diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py
new file mode 100644
index 0000000..e71bd44
--- /dev/null
+++ b/tests/contrib/test_gce.py
@@ -0,0 +1,152 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for oauth2client.contrib.gce."""
+
+import datetime
+import json
+
+import httplib2
+import mock
+from six.moves import http_client
+from tests.contrib.test_metadata import request_mock
+import unittest2
+
+from oauth2client import client
+from oauth2client.contrib import gce
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+SERVICE_ACCOUNT_INFO = {
+    'scopes': ['a', 'b'],
+    'email': 'a@example.com',
+    'aliases': ['default']
+}
+
+
+class AppAssertionCredentialsTests(unittest2.TestCase):
+
+    def test_constructor(self):
+        credentials = gce.AppAssertionCredentials()
+        self.assertIsNone(credentials.assertion_type, None)
+        self.assertIsNone(credentials.service_account_email)
+        self.assertIsNone(credentials.scopes)
+        self.assertTrue(credentials.invalid)
+
+    @mock.patch('warnings.warn')
+    def test_constructor_with_scopes(self, warn_mock):
+        scope = 'http://example.com/a http://example.com/b'
+        scopes = scope.split()
+        credentials = gce.AppAssertionCredentials(scopes=scopes)
+        self.assertEqual(credentials.scopes, None)
+        self.assertEqual(credentials.assertion_type, None)
+        warn_mock.assert_called_once_with(gce._SCOPES_WARNING)
+
+    def test_to_json(self):
+        credentials = gce.AppAssertionCredentials()
+        with self.assertRaises(NotImplementedError):
+            credentials.to_json()
+
+    def test_from_json(self):
+        with self.assertRaises(NotImplementedError):
+            gce.AppAssertionCredentials.from_json({})
+
+    @mock.patch('oauth2client.contrib._metadata.get_token',
+                side_effect=[('A', datetime.datetime.min),
+                             ('B', datetime.datetime.max)])
+    @mock.patch('oauth2client.contrib._metadata.get_service_account_info',
+                return_value=SERVICE_ACCOUNT_INFO)
+    def test_refresh_token(self, get_info, get_token):
+        http_request = mock.MagicMock()
+        http_mock = mock.MagicMock(request=http_request)
+        credentials = gce.AppAssertionCredentials()
+        credentials.invalid = False
+        credentials.service_account_email = 'a@example.com'
+        self.assertIsNone(credentials.access_token)
+        credentials.get_access_token(http=http_mock)
+        self.assertEqual(credentials.access_token, 'A')
+        self.assertTrue(credentials.access_token_expired)
+        get_token.assert_called_with(http_request,
+                                     service_account='a@example.com')
+        credentials.get_access_token(http=http_mock)
+        self.assertEqual(credentials.access_token, 'B')
+        self.assertFalse(credentials.access_token_expired)
+        get_token.assert_called_with(http_request,
+                                     service_account='a@example.com')
+        get_info.assert_not_called()
+
+    def test_refresh_token_failed_fetch(self):
+        http_request = request_mock(
+            http_client.NOT_FOUND,
+            'application/json',
+            json.dumps({'access_token': 'a', 'expires_in': 100})
+        )
+        credentials = gce.AppAssertionCredentials()
+        credentials.invalid = False
+        credentials.service_account_email = 'a@example.com'
+        with self.assertRaises(client.HttpAccessTokenRefreshError):
+            credentials._refresh(http_request)
+
+    def test_serialization_data(self):
+        credentials = gce.AppAssertionCredentials()
+        with self.assertRaises(NotImplementedError):
+            getattr(credentials, 'serialization_data')
+
+    def test_create_scoped_required(self):
+        credentials = gce.AppAssertionCredentials()
+        self.assertFalse(credentials.create_scoped_required())
+
+    def test_sign_blob_not_implemented(self):
+        credentials = gce.AppAssertionCredentials([])
+        with self.assertRaises(NotImplementedError):
+            credentials.sign_blob(b'blob')
+
+    @mock.patch('oauth2client.contrib._metadata.get_service_account_info',
+                return_value=SERVICE_ACCOUNT_INFO)
+    def test_retrieve_scopes(self, metadata):
+        http_request = mock.MagicMock()
+        http_mock = mock.MagicMock(request=http_request)
+        credentials = gce.AppAssertionCredentials()
+        self.assertTrue(credentials.invalid)
+        self.assertIsNone(credentials.scopes)
+        scopes = credentials.retrieve_scopes(http_mock)
+        self.assertEqual(scopes, SERVICE_ACCOUNT_INFO['scopes'])
+        self.assertFalse(credentials.invalid)
+        credentials.retrieve_scopes(http_mock)
+        # Assert scopes weren't refetched
+        metadata.assert_called_once_with(http_request,
+                                         service_account='default')
+
+    @mock.patch('oauth2client.contrib._metadata.get_service_account_info',
+                side_effect=httplib2.HttpLib2Error('No Such Email'))
+    def test_retrieve_scopes_bad_email(self, metadata):
+        http_request = mock.MagicMock()
+        http_mock = mock.MagicMock(request=http_request)
+        credentials = gce.AppAssertionCredentials(email='b@example.com')
+        with self.assertRaises(httplib2.HttpLib2Error):
+            credentials.retrieve_scopes(http_mock)
+
+        metadata.assert_called_once_with(http_request,
+                                         service_account='b@example.com')
+
+    def test_save_to_well_known_file(self):
+        import os
+        ORIGINAL_ISDIR = os.path.isdir
+        try:
+            os.path.isdir = lambda path: True
+            credentials = gce.AppAssertionCredentials()
+            with self.assertRaises(NotImplementedError):
+                client.save_to_well_known_file(credentials)
+        finally:
+            os.path.isdir = ORIGINAL_ISDIR
diff --git a/tests/contrib/test_keyring_storage.py b/tests/contrib/test_keyring_storage.py
new file mode 100644
index 0000000..5d274c0
--- /dev/null
+++ b/tests/contrib/test_keyring_storage.py
@@ -0,0 +1,171 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Tests for oauth2client.contrib.keyring_storage."""
+
+import datetime
+import threading
+
+import keyring
+import mock
+import unittest2
+
+import oauth2client
+from oauth2client import client
+from oauth2client.contrib import keyring_storage
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class KeyringStorageTests(unittest2.TestCase):
+
+    def test_constructor(self):
+        service_name = 'my_unit_test'
+        user_name = 'me'
+        store = keyring_storage.Storage(service_name, user_name)
+        self.assertEqual(store._service_name, service_name)
+        self.assertEqual(store._user_name, user_name)
+        lock_type = type(threading.Lock())
+        self.assertIsInstance(store._lock, lock_type)
+
+    def test_acquire_lock(self):
+        store = keyring_storage.Storage('my_unit_test', 'me')
+        store._lock = lock = _FakeLock()
+        self.assertEqual(lock._acquire_count, 0)
+        store.acquire_lock()
+        self.assertEqual(lock._acquire_count, 1)
+
+    def test_release_lock(self):
+        store = keyring_storage.Storage('my_unit_test', 'me')
+        store._lock = lock = _FakeLock()
+        self.assertEqual(lock._release_count, 0)
+        store.release_lock()
+        self.assertEqual(lock._release_count, 1)
+
+    def test_locked_get(self):
+        service_name = 'my_unit_test'
+        user_name = 'me'
+        mock_content = (object(), 'mock_content')
+        mock_return_creds = mock.MagicMock()
+        mock_return_creds.set_store = set_store = mock.MagicMock(
+            name='set_store')
+        with mock.patch.object(keyring, 'get_password',
+                               return_value=mock_content,
+                               autospec=True) as get_password:
+            class_name = 'oauth2client.client.Credentials'
+            with mock.patch(class_name) as MockCreds:
+                MockCreds.new_from_json = new_from_json = mock.MagicMock(
+                    name='new_from_json', return_value=mock_return_creds)
+                store = keyring_storage.Storage(service_name, user_name)
+                credentials = store.locked_get()
+                new_from_json.assert_called_once_with(mock_content)
+                get_password.assert_called_once_with(service_name, user_name)
+                self.assertEqual(credentials, mock_return_creds)
+                set_store.assert_called_once_with(store)
+
+    def test_locked_put(self):
+        service_name = 'my_unit_test'
+        user_name = 'me'
+        store = keyring_storage.Storage(service_name, user_name)
+        with mock.patch.object(keyring, 'set_password',
+                               return_value=None,
+                               autospec=True) as set_password:
+            credentials = mock.MagicMock()
+            to_json_ret = object()
+            credentials.to_json = to_json = mock.MagicMock(
+                name='to_json', return_value=to_json_ret)
+            store.locked_put(credentials)
+            to_json.assert_called_once_with()
+            set_password.assert_called_once_with(service_name, user_name,
+                                                 to_json_ret)
+
+    def test_locked_delete(self):
+        service_name = 'my_unit_test'
+        user_name = 'me'
+        store = keyring_storage.Storage(service_name, user_name)
+        with mock.patch.object(keyring, 'set_password',
+                               return_value=None,
+                               autospec=True) as set_password:
+            store.locked_delete()
+            set_password.assert_called_once_with(service_name, user_name, '')
+
+    def test_get_with_no_credentials_stored(self):
+        with mock.patch.object(keyring, 'get_password',
+                               return_value=None,
+                               autospec=True) as get_password:
+            store = keyring_storage.Storage('my_unit_test', 'me')
+            credentials = store.get()
+            self.assertEquals(None, credentials)
+            get_password.assert_called_once_with('my_unit_test', 'me')
+
+    def test_get_with_malformed_json_credentials_stored(self):
+        with mock.patch.object(keyring, 'get_password',
+                               return_value='{',
+                               autospec=True) as get_password:
+            store = keyring_storage.Storage('my_unit_test', 'me')
+            credentials = store.get()
+            self.assertEquals(None, credentials)
+            get_password.assert_called_once_with('my_unit_test', 'me')
+
+    def test_get_and_set_with_json_credentials_stored(self):
+        access_token = 'foo'
+        client_id = 'some_client_id'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = datetime.datetime.utcnow()
+        user_agent = 'refresh_checker/1.0'
+
+        credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, oauth2client.GOOGLE_TOKEN_URI,
+            user_agent)
+
+        # Setting autospec on a mock with an iterable side_effect is
+        # currently broken (http://bugs.python.org/issue17826), so instead
+        # we patch twice.
+        with mock.patch.object(keyring, 'get_password',
+                               return_value=None,
+                               autospec=True) as get_password:
+            with mock.patch.object(keyring, 'set_password',
+                                   return_value=None,
+                                   autospec=True) as set_password:
+                store = keyring_storage.Storage('my_unit_test', 'me')
+                self.assertEquals(None, store.get())
+
+                store.put(credentials)
+
+                set_password.assert_called_once_with(
+                    'my_unit_test', 'me', credentials.to_json())
+                get_password.assert_called_once_with('my_unit_test', 'me')
+
+        with mock.patch.object(keyring, 'get_password',
+                               return_value=credentials.to_json(),
+                               autospec=True) as get_password:
+            restored = store.get()
+            self.assertEqual('foo', restored.access_token)
+            self.assertEqual('some_client_id', restored.client_id)
+            get_password.assert_called_once_with('my_unit_test', 'me')
+
+
+class _FakeLock(object):
+
+    _acquire_count = 0
+    _release_count = 0
+
+    def acquire(self):
+        self._acquire_count += 1
+
+    def release(self):
+        self._release_count += 1
diff --git a/tests/contrib/test_locked_file.py b/tests/contrib/test_locked_file.py
new file mode 100644
index 0000000..384bef3
--- /dev/null
+++ b/tests/contrib/test_locked_file.py
@@ -0,0 +1,244 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import errno
+import os
+import sys
+import tempfile
+
+import mock
+import unittest2
+
+from oauth2client.contrib import locked_file
+
+
+class TestOpener(unittest2.TestCase):
+    def _make_one(self):
+        _filehandle, filename = tempfile.mkstemp()
+        os.close(_filehandle)
+        return locked_file._Opener(filename, 'r+', 'r'), filename
+
+    def test_ctor(self):
+        instance, filename = self._make_one()
+        self.assertFalse(instance._locked)
+        self.assertEqual(instance._filename, filename)
+        self.assertEqual(instance._mode, 'r+')
+        self.assertEqual(instance._fallback_mode, 'r')
+        self.assertIsNone(instance._fh)
+        self.assertIsNone(instance._lock_fd)
+
+    def test_is_locked(self):
+        instance, _ = self._make_one()
+        self.assertFalse(instance.is_locked())
+        instance._locked = True
+        self.assertTrue(instance.is_locked())
+
+    def test_file_handle(self):
+        instance, _ = self._make_one()
+        self.assertIsNone(instance.file_handle())
+        fh = mock.Mock()
+        instance._fh = fh
+        self.assertEqual(instance.file_handle(), fh)
+
+    def test_filename(self):
+        instance, filename = self._make_one()
+        self.assertEqual(instance.filename(), filename)
+
+    def test_open_and_lock(self):
+        instance, _ = self._make_one()
+        instance.open_and_lock(1, 1)
+
+    def test_unlock_and_close(self):
+        instance, _ = self._make_one()
+        instance.unlock_and_close()
+
+
+class TestPosixOpener(TestOpener):
+    def _make_one(self):
+        _filehandle, filename = tempfile.mkstemp()
+        os.close(_filehandle)
+        return locked_file._PosixOpener(filename, 'r+', 'r'), filename
+
+    def test_relock_fail(self):
+        instance, _ = self._make_one()
+        instance.open_and_lock(1, 1)
+
+        self.assertTrue(instance.is_locked())
+        self.assertIsNotNone(instance.file_handle())
+        with self.assertRaises(locked_file.AlreadyLockedException):
+            instance.open_and_lock(1, 1)
+
+    @mock.patch('oauth2client.contrib.locked_file.open', create=True)
+    def test_lock_access_error_fallback_mode(self, mock_open):
+        # NOTE: This is a bad case. The behavior here should be that the
+        # error gets re-raised, but the module lets the if statement fall
+        # through.
+        instance, _ = self._make_one()
+        mock_open.side_effect = [IOError(errno.ENOENT, '')]
+        instance.open_and_lock(1, 1)
+
+        self.assertIsNone(instance.file_handle())
+        self.assertTrue(instance.is_locked())
+
+    @mock.patch('oauth2client.contrib.locked_file.open', create=True)
+    def test_lock_non_access_error(self, mock_open):
+        instance, _ = self._make_one()
+        fh_mock = mock.Mock()
+        mock_open.side_effect = [IOError(errno.EACCES, ''), fh_mock]
+        instance.open_and_lock(1, 1)
+
+        self.assertEqual(instance.file_handle(), fh_mock)
+        self.assertFalse(instance.is_locked())
+
+    @mock.patch('oauth2client.contrib.locked_file.open', create=True)
+    def test_lock_unexpected_error(self, mock_open):
+        instance, _ = self._make_one()
+
+        with mock.patch('os.open') as mock_os_open:
+            mock_os_open.side_effect = [OSError(errno.EPERM, '')]
+            with self.assertRaises(OSError):
+                instance.open_and_lock(1, 1)
+
+    @mock.patch('oauth2client.contrib.locked_file.open', create=True)
+    @mock.patch('oauth2client.contrib.locked_file.logger')
+    @mock.patch('time.time')
+    def test_lock_timeout_error(self, mock_time, mock_logger, mock_open):
+        instance, _ = self._make_one()
+        # Make it seem like 10 seconds have passed between calls.
+        mock_time.side_effect = [0, 10]
+
+        with mock.patch('os.open') as mock_os_open:
+            # Raising EEXIST should cause it to try to retry locking.
+            mock_os_open.side_effect = [OSError(errno.EEXIST, '')]
+            instance.open_and_lock(1, 1)
+            self.assertFalse(instance.is_locked())
+            self.assertTrue(mock_logger.warn.called)
+
+    @mock.patch('oauth2client.contrib.locked_file.open', create=True)
+    @mock.patch('oauth2client.contrib.locked_file.logger')
+    @mock.patch('time.time')
+    def test_lock_timeout_error_no_fh(self, mock_time, mock_logger, mock_open):
+        instance, _ = self._make_one()
+        # Make it seem like 10 seconds have passed between calls.
+        mock_time.side_effect = [0, 10]
+        # This will cause the retry loop to enter without a file handle.
+        fh_mock = mock.Mock()
+        mock_open.side_effect = [IOError(errno.ENOENT, ''), fh_mock]
+
+        with mock.patch('os.open') as mock_os_open:
+            # Raising EEXIST should cause it to try to retry locking.
+            mock_os_open.side_effect = [OSError(errno.EEXIST, '')]
+            instance.open_and_lock(1, 1)
+            self.assertFalse(instance.is_locked())
+            self.assertTrue(mock_logger.warn.called)
+            self.assertEqual(instance.file_handle(), fh_mock)
+
+    @mock.patch('oauth2client.contrib.locked_file.open', create=True)
+    @mock.patch('time.time')
+    @mock.patch('time.sleep')
+    def test_lock_retry_success(self, mock_sleep, mock_time, mock_open):
+        instance, _ = self._make_one()
+        # Make it seem like 1 second has passed between calls. Extra values
+        # are needed by the logging module.
+        mock_time.side_effect = [0, 1]
+
+        with mock.patch('os.open') as mock_os_open:
+            # Raising EEXIST should cause it to try to retry locking.
+            mock_os_open.side_effect = [
+                OSError(errno.EEXIST, ''), mock.Mock()]
+            instance.open_and_lock(10, 1)
+            print(mock_os_open.call_args_list)
+            self.assertTrue(instance.is_locked())
+            mock_sleep.assert_called_with(1)
+
+    @mock.patch('oauth2client.contrib.locked_file.os')
+    def test_unlock(self, os_mock):
+        instance, _ = self._make_one()
+        instance._locked = True
+        lock_fd_mock = instance._lock_fd = mock.Mock()
+        instance._fh = mock.Mock()
+
+        instance.unlock_and_close()
+
+        self.assertFalse(instance.is_locked())
+        os_mock.close.assert_called_once_with(lock_fd_mock)
+        self.assertTrue(os_mock.unlink.called)
+        self.assertTrue(instance._fh.close.called)
+
+
+class TestLockedFile(unittest2.TestCase):
+
+    @mock.patch('oauth2client.contrib.locked_file._PosixOpener')
+    def _make_one(self, opener_ctor_mock):
+        opener_mock = mock.Mock()
+        opener_ctor_mock.return_value = opener_mock
+        return locked_file.LockedFile(
+            'a_file', 'r+', 'r', use_native_locking=False), opener_mock
+
+    @mock.patch('oauth2client.contrib.locked_file._PosixOpener')
+    def test_ctor_minimal(self, opener_mock):
+        locked_file.LockedFile(
+            'a_file', 'r+', 'r', use_native_locking=False)
+        opener_mock.assert_called_with('a_file', 'r+', 'r')
+
+    @mock.patch.dict('sys.modules', {
+        'oauth2client.contrib._win32_opener': mock.Mock()})
+    def test_ctor_native_win32(self):
+        _win32_opener_mock = sys.modules['oauth2client.contrib._win32_opener']
+        locked_file.LockedFile(
+            'a_file', 'r+', 'r', use_native_locking=True)
+        _win32_opener_mock._Win32Opener.assert_called_with('a_file', 'r+', 'r')
+
+    @mock.patch.dict('sys.modules', {
+        'oauth2client.contrib._win32_opener': None,
+        'oauth2client.contrib._fcntl_opener': mock.Mock()})
+    def test_ctor_native_fcntl(self):
+        _fnctl_opener_mock = sys.modules['oauth2client.contrib._fcntl_opener']
+        locked_file.LockedFile(
+            'a_file', 'r+', 'r', use_native_locking=True)
+        _fnctl_opener_mock._FcntlOpener.assert_called_with('a_file', 'r+', 'r')
+
+    @mock.patch('oauth2client.contrib.locked_file._PosixOpener')
+    @mock.patch.dict('sys.modules', {
+        'oauth2client.contrib._win32_opener': None,
+        'oauth2client.contrib._fcntl_opener': None})
+    def test_ctor_native_posix_fallback(self, opener_mock):
+        locked_file.LockedFile(
+            'a_file', 'r+', 'r', use_native_locking=True)
+        opener_mock.assert_called_with('a_file', 'r+', 'r')
+
+    def test_filename(self):
+        instance, opener = self._make_one()
+        opener._filename = 'some file'
+        self.assertEqual(instance.filename(), 'some file')
+
+    def test_file_handle(self):
+        instance, opener = self._make_one()
+        self.assertEqual(instance.file_handle(), opener.file_handle())
+        self.assertTrue(opener.file_handle.called)
+
+    def test_is_locked(self):
+        instance, opener = self._make_one()
+        self.assertEqual(instance.is_locked(), opener.is_locked())
+        self.assertTrue(opener.is_locked.called)
+
+    def test_open_and_lock(self):
+        instance, opener = self._make_one()
+        instance.open_and_lock()
+        opener.open_and_lock.assert_called_with(0, 0.05)
+
+    def test_unlock_and_close(self):
+        instance, opener = self._make_one()
+        instance.unlock_and_close()
+        opener.unlock_and_close.assert_called_with()
diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py
new file mode 100644
index 0000000..7f11d04
--- /dev/null
+++ b/tests/contrib/test_metadata.py
@@ -0,0 +1,97 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import datetime
+import json
+
+import httplib2
+import mock
+from six.moves import http_client
+import unittest2
+
+from oauth2client.contrib import _metadata
+
+PATH = 'instance/service-accounts/default'
+DATA = {'foo': 'bar'}
+EXPECTED_URL = (
+    'http://metadata.google.internal/computeMetadata/v1/instance'
+    '/service-accounts/default')
+EXPECTED_KWARGS = dict(headers=_metadata.METADATA_HEADERS)
+
+
+def request_mock(status, content_type, content):
+    return mock.MagicMock(return_value=(
+        httplib2.Response(
+            {'status': status, 'content-type': content_type}
+        ),
+        content.encode('utf-8')
+    ))
+
+
+class TestMetadata(unittest2.TestCase):
+
+    def test_get_success_json(self):
+        http_request = request_mock(
+            http_client.OK, 'application/json', json.dumps(DATA))
+        self.assertEqual(
+            _metadata.get(http_request, PATH),
+            DATA
+        )
+        http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS)
+
+    def test_get_success_string(self):
+        http_request = request_mock(
+            http_client.OK, 'text/html', '<p>Hello World!</p>')
+        self.assertEqual(
+            _metadata.get(http_request, PATH),
+            '<p>Hello World!</p>'
+        )
+        http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS)
+
+    def test_get_failure(self):
+        http_request = request_mock(
+            http_client.NOT_FOUND, 'text/html', '<p>Error</p>')
+        with self.assertRaises(httplib2.HttpLib2Error):
+            _metadata.get(http_request, PATH)
+
+        http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS)
+
+    @mock.patch(
+        'oauth2client.client._UTCNOW',
+        return_value=datetime.datetime.min)
+    def test_get_token_success(self, now):
+        http_request = request_mock(
+            http_client.OK,
+            'application/json',
+            json.dumps({'access_token': 'a', 'expires_in': 100})
+        )
+        token, expiry = _metadata.get_token(http_request=http_request)
+        self.assertEqual(token, 'a')
+        self.assertEqual(
+            expiry, datetime.datetime.min + datetime.timedelta(seconds=100))
+        http_request.assert_called_once_with(
+            EXPECTED_URL + '/token',
+            **EXPECTED_KWARGS
+        )
+        now.assert_called_once_with()
+
+    def test_service_account_info(self):
+        http_request = request_mock(
+            http_client.OK, 'application/json', json.dumps(DATA))
+        info = _metadata.get_service_account_info(http_request)
+        self.assertEqual(info, DATA)
+        http_request.assert_called_once_with(
+            EXPECTED_URL + '/?recursive=True',
+            **EXPECTED_KWARGS
+        )
diff --git a/tests/contrib/test_multiprocess_file_storage.py b/tests/contrib/test_multiprocess_file_storage.py
new file mode 100644
index 0000000..bf30c14
--- /dev/null
+++ b/tests/contrib/test_multiprocess_file_storage.py
@@ -0,0 +1,313 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for oauth2client.multistore_file."""
+
+import contextlib
+import datetime
+import json
+import multiprocessing
+import os
+import tempfile
+
+import fasteners
+import mock
+from six import StringIO
+import unittest2
+
+from oauth2client import client
+from oauth2client.contrib import multiprocess_file_storage
+
+from ..http_mock import HttpMockSequence
+
+
+@contextlib.contextmanager
+def scoped_child_process(target, **kwargs):
+    die_event = multiprocessing.Event()
+    ready_event = multiprocessing.Event()
+    process = multiprocessing.Process(
+        target=target, args=(die_event, ready_event), kwargs=kwargs)
+    process.start()
+    try:
+        ready_event.wait()
+        yield
+    finally:
+        die_event.set()
+        process.join(5)
+
+
+def _create_test_credentials(expiration=None):
+    access_token = 'foo'
+    client_secret = 'cOuDdkfjxxnv+'
+    refresh_token = '1/0/a.df219fjls0'
+    token_expiry = expiration or (
+        datetime.datetime.utcnow() + datetime.timedelta(seconds=3600))
+    token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
+    user_agent = 'refresh_checker/1.0'
+
+    credentials = client.OAuth2Credentials(
+        access_token, 'test-client-id', client_secret,
+        refresh_token, token_expiry, token_uri,
+        user_agent)
+    return credentials
+
+
+def _generate_token_response_http(new_token='new_token'):
+    token_response = json.dumps({
+        'access_token': new_token,
+        'expires_in': '3600',
+    })
+    http = HttpMockSequence([
+        ({'status': '200'}, token_response),
+    ])
+
+    return http
+
+
+class MultiprocessStorageBehaviorTests(unittest2.TestCase):
+
+    def setUp(self):
+        filehandle, self.filename = tempfile.mkstemp(
+            'oauth2client_test.data')
+        os.close(filehandle)
+
+    def tearDown(self):
+        try:
+            os.unlink(self.filename)
+            os.unlink('{0}.lock'.format(self.filename))
+        except OSError:  # pragma: NO COVER
+            pass
+
+    def test_basic_operations(self):
+        credentials = _create_test_credentials()
+
+        store = multiprocess_file_storage.MultiprocessFileStorage(
+            self.filename, 'basic')
+
+        # Save credentials
+        store.put(credentials)
+        credentials = store.get()
+
+        self.assertIsNotNone(credentials)
+        self.assertEqual('foo', credentials.access_token)
+
+        # Reset internal cache, ensure credentials were saved.
+        store._backend._credentials = {}
+        credentials = store.get()
+
+        self.assertIsNotNone(credentials)
+        self.assertEqual('foo', credentials.access_token)
+
+        # Delete credentials
+        store.delete()
+        credentials = store.get()
+
+        self.assertIsNone(credentials)
+
+    def test_single_process_refresh(self):
+        store = multiprocess_file_storage.MultiprocessFileStorage(
+            self.filename, 'single-process')
+        credentials = _create_test_credentials()
+        credentials.set_store(store)
+
+        http = _generate_token_response_http()
+        credentials.refresh(http)
+        self.assertEqual(credentials.access_token, 'new_token')
+
+        retrieved = store.get()
+        self.assertEqual(retrieved.access_token, 'new_token')
+
+    def test_multi_process_refresh(self):
+        # This will test that two processes attempting to refresh credentials
+        # will only refresh once.
+        store = multiprocess_file_storage.MultiprocessFileStorage(
+            self.filename, 'multi-process')
+        credentials = _create_test_credentials()
+        credentials.set_store(store)
+        store.put(credentials)
+
+        def child_process_func(
+                die_event, ready_event, check_event):  # pragma: NO COVER
+            store = multiprocess_file_storage.MultiprocessFileStorage(
+                self.filename, 'multi-process')
+
+            credentials = store.get()
+            self.assertIsNotNone(credentials)
+
+            # Make sure this thread gets to refresh first.
+            original_acquire_lock = store.acquire_lock
+
+            def replacement_acquire_lock(*args, **kwargs):
+                result = original_acquire_lock(*args, **kwargs)
+                ready_event.set()
+                check_event.wait()
+                return result
+
+            credentials.store.acquire_lock = replacement_acquire_lock
+
+            http = _generate_token_response_http('b')
+            credentials.refresh(http)
+
+            self.assertEqual(credentials.access_token, 'b')
+
+        check_event = multiprocessing.Event()
+        with scoped_child_process(child_process_func, check_event=check_event):
+            # The lock should be currently held by the child process.
+            self.assertFalse(
+                store._backend._process_lock.acquire(blocking=False))
+            check_event.set()
+
+            # The child process will refresh first, so we should end up
+            # with 'b' as the token.
+            http = mock.Mock()
+            credentials.refresh(http=http)
+            self.assertEqual(credentials.access_token, 'b')
+            self.assertFalse(http.request.called)
+
+        retrieved = store.get()
+        self.assertEqual(retrieved.access_token, 'b')
+
+    def test_read_only_file_fail_lock(self):
+        credentials = _create_test_credentials()
+
+        # Grab the lock in another process, preventing this process from
+        # acquiring the lock.
+        def child_process(die_event, ready_event):  # pragma: NO COVER
+            lock = fasteners.InterProcessLock(
+                '{0}.lock'.format(self.filename))
+            with lock:
+                ready_event.set()
+                die_event.wait()
+
+        with scoped_child_process(child_process):
+            store = multiprocess_file_storage.MultiprocessFileStorage(
+                self.filename, 'fail-lock')
+            store.put(credentials)
+            self.assertTrue(store._backend._read_only)
+
+        # These credentials should still be in the store's memory-only cache.
+        self.assertIsNotNone(store.get())
+
+
+class MultiprocessStorageUnitTests(unittest2.TestCase):
+
+    def setUp(self):
+        filehandle, self.filename = tempfile.mkstemp(
+            'oauth2client_test.data')
+        os.close(filehandle)
+
+    def tearDown(self):
+        try:
+            os.unlink(self.filename)
+            os.unlink('{0}.lock'.format(self.filename))
+        except OSError:  # pragma: NO COVER
+            pass
+
+    def test__create_file_if_needed(self):
+        self.assertFalse(
+            multiprocess_file_storage._create_file_if_needed(self.filename))
+        os.unlink(self.filename)
+        self.assertTrue(
+            multiprocess_file_storage._create_file_if_needed(self.filename))
+        self.assertTrue(
+            os.path.exists(self.filename))
+
+    def test__get_backend(self):
+        backend_one = multiprocess_file_storage._get_backend('file_a')
+        backend_two = multiprocess_file_storage._get_backend('file_a')
+        backend_three = multiprocess_file_storage._get_backend('file_b')
+
+        self.assertIs(backend_one, backend_two)
+        self.assertIsNot(backend_one, backend_three)
+
+    def test__read_write_credentials_file(self):
+        credentials = _create_test_credentials()
+        contents = StringIO()
+
+        multiprocess_file_storage._write_credentials_file(
+            contents, {'key': credentials})
+
+        contents.seek(0)
+        data = json.load(contents)
+        self.assertEqual(data['file_version'], 2)
+        self.assertTrue(data['credentials']['key'])
+
+        # Read it back.
+        contents.seek(0)
+        results = multiprocess_file_storage._load_credentials_file(contents)
+        self.assertEqual(
+            results['key'].access_token, credentials.access_token)
+
+        # Add an invalid credential and try reading it back. It should ignore
+        # the invalid one but still load the valid one.
+        data['credentials']['invalid'] = '123'
+        results = multiprocess_file_storage._load_credentials_file(
+            StringIO(json.dumps(data)))
+        self.assertNotIn('invalid', results)
+        self.assertEqual(
+            results['key'].access_token, credentials.access_token)
+
+    def test__load_credentials_file_invalid_json(self):
+        contents = StringIO('{[')
+        self.assertEqual(
+            multiprocess_file_storage._load_credentials_file(contents), {})
+
+    def test__load_credentials_file_no_file_version(self):
+        contents = StringIO('{}')
+        self.assertEqual(
+            multiprocess_file_storage._load_credentials_file(contents), {})
+
+    def test__load_credentials_file_bad_file_version(self):
+        contents = StringIO(json.dumps({'file_version': 1}))
+        self.assertEqual(
+            multiprocess_file_storage._load_credentials_file(contents), {})
+
+    def test__load_credentials_no_open_file(self):
+        backend = multiprocess_file_storage._get_backend(self.filename)
+        backend._credentials = mock.Mock()
+        backend._credentials.update.side_effect = AssertionError()
+        backend._load_credentials()
+
+    def test_acquire_lock_nonexistent_file(self):
+        backend = multiprocess_file_storage._get_backend(self.filename)
+        os.unlink(self.filename)
+        backend._process_lock = mock.Mock()
+        backend._process_lock.acquire.return_value = False
+        backend.acquire_lock()
+        self.assertIsNone(backend._file)
+
+    def test_release_lock_with_no_file(self):
+        backend = multiprocess_file_storage._get_backend(self.filename)
+        backend._file = None
+        backend._read_only = True
+        backend._thread_lock.acquire()
+        backend.release_lock()
+
+    def test__refresh_predicate(self):
+        backend = multiprocess_file_storage._get_backend(self.filename)
+
+        credentials = _create_test_credentials()
+        self.assertFalse(backend._refresh_predicate(credentials))
+
+        credentials.invalid = True
+        self.assertTrue(backend._refresh_predicate(credentials))
+
+        credentials = _create_test_credentials(
+            expiration=(
+                datetime.datetime.utcnow() - datetime.timedelta(seconds=3600)))
+        self.assertTrue(backend._refresh_predicate(credentials))
+
+
+if __name__ == '__main__':  # pragma: NO COVER
+    unittest2.main()
diff --git a/tests/contrib/test_multistore_file.py b/tests/contrib/test_multistore_file.py
new file mode 100644
index 0000000..b5cb598
--- /dev/null
+++ b/tests/contrib/test_multistore_file.py
@@ -0,0 +1,383 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for oauth2client.multistore_file."""
+
+import datetime
+import errno
+import os
+import stat
+import tempfile
+
+import mock
+import unittest2
+
+from oauth2client import client
+from oauth2client import util
+from oauth2client.contrib import locked_file
+from oauth2client.contrib import multistore_file
+
+_filehandle, FILENAME = tempfile.mkstemp('oauth2client_test.data')
+os.close(_filehandle)
+
+
+class _MockLockedFile(object):
+
+    def __init__(self, filename_str, error_class, error_code):
+        self.filename_str = filename_str
+        self.error_class = error_class
+        self.error_code = error_code
+        self.open_and_lock_called = False
+
+    def open_and_lock(self):
+        self.open_and_lock_called = True
+        raise self.error_class(self.error_code, '')
+
+    def is_locked(self):
+        return False
+
+    def filename(self):
+        return self.filename_str
+
+
+class Test__dict_to_tuple_key(unittest2.TestCase):
+
+    def test_key_conversions(self):
+        key1, val1 = 'somekey', 'some value'
+        key2, val2 = 'another', 'something else'
+        key3, val3 = 'onemore', 'foo'
+        test_dict = {
+            key1: val1,
+            key2: val2,
+            key3: val3,
+        }
+        tuple_key = multistore_file._dict_to_tuple_key(test_dict)
+
+        # the resulting key should be naturally sorted
+        expected_output = (
+            (key2, val2),
+            (key3, val3),
+            (key1, val1),
+        )
+        self.assertTupleEqual(expected_output, tuple_key)
+        # check we get the original dictionary back
+        self.assertDictEqual(test_dict, dict(tuple_key))
+
+
+class MultistoreFileTests(unittest2.TestCase):
+
+    def tearDown(self):
+        try:
+            os.unlink(FILENAME)
+        except OSError:
+            pass
+
+    def setUp(self):
+        try:
+            os.unlink(FILENAME)
+        except OSError:
+            pass
+
+    def _create_test_credentials(self, client_id='some_client_id',
+                                 expiration=None):
+        access_token = 'foo'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = expiration or datetime.datetime.utcnow()
+        token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
+        user_agent = 'refresh_checker/1.0'
+
+        credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, token_uri,
+            user_agent)
+        return credentials
+
+    def test_lock_file_raises_ioerror(self):
+        filehandle, filename = tempfile.mkstemp()
+        os.close(filehandle)
+
+        try:
+            for error_code in (errno.EDEADLK, errno.ENOSYS, errno.ENOLCK,
+                               errno.EACCES):
+                for error_class in (IOError, OSError):
+                    multistore = multistore_file._MultiStore(filename)
+                    multistore._file = _MockLockedFile(
+                        filename, error_class, error_code)
+                    # Should not raise though the underlying file class did.
+                    multistore._lock()
+                    self.assertTrue(multistore._file.open_and_lock_called)
+        finally:
+            os.unlink(filename)
+
+    def test_lock_file_raise_unexpected_error(self):
+        filehandle, filename = tempfile.mkstemp()
+        os.close(filehandle)
+
+        try:
+            multistore = multistore_file._MultiStore(filename)
+            multistore._file = _MockLockedFile(filename, IOError, errno.EBUSY)
+            with self.assertRaises(IOError):
+                multistore._lock()
+            self.assertTrue(multistore._file.open_and_lock_called)
+        finally:
+            os.unlink(filename)
+
+    def test_read_only_file_fail_lock(self):
+        credentials = self._create_test_credentials()
+
+        open(FILENAME, 'a+b').close()
+        os.chmod(FILENAME, 0o400)
+
+        store = multistore_file.get_credential_storage(
+            FILENAME,
+            credentials.client_id,
+            credentials.user_agent,
+            ['some-scope', 'some-other-scope'])
+
+        store.put(credentials)
+        if os.name == 'posix':  # pragma: NO COVER
+            self.assertTrue(store._multistore._read_only)
+        os.chmod(FILENAME, 0o600)
+
+    def test_read_only_file_fail_lock_no_warning(self):
+        open(FILENAME, 'a+b').close()
+        os.chmod(FILENAME, 0o400)
+
+        multistore = multistore_file._MultiStore(FILENAME)
+
+        with mock.patch.object(multistore_file.logger, 'warn') as mock_warn:
+            multistore._warn_on_readonly = False
+            multistore._lock()
+            self.assertFalse(mock_warn.called)
+
+    def test_lock_skip_refresh(self):
+        with open(FILENAME, 'w') as f:
+            f.write('123')
+        os.chmod(FILENAME, 0o400)
+
+        multistore = multistore_file._MultiStore(FILENAME)
+
+        refresh_patch = mock.patch.object(
+            multistore, '_refresh_data_cache')
+
+        with refresh_patch as refresh_mock:
+            multistore._data = {}
+            multistore._lock()
+            self.assertFalse(refresh_mock.called)
+
+    @unittest2.skipIf(not hasattr(os, 'symlink'), 'No symlink available')
+    def test_multistore_no_symbolic_link_files(self):
+        SYMFILENAME = FILENAME + 'sym'
+        os.symlink(FILENAME, SYMFILENAME)
+        store = multistore_file.get_credential_storage(
+            SYMFILENAME,
+            'some_client_id',
+            'user-agent/1.0',
+            ['some-scope', 'some-other-scope'])
+        try:
+            with self.assertRaises(
+                    locked_file.CredentialsFileSymbolicLinkError):
+                store.get()
+        finally:
+            os.unlink(SYMFILENAME)
+
+    def test_multistore_non_existent_file(self):
+        store = multistore_file.get_credential_storage(
+            FILENAME,
+            'some_client_id',
+            'user-agent/1.0',
+            ['some-scope', 'some-other-scope'])
+
+        credentials = store.get()
+        self.assertEquals(None, credentials)
+
+    def test_multistore_file(self):
+        credentials = self._create_test_credentials()
+
+        store = multistore_file.get_credential_storage(
+            FILENAME,
+            credentials.client_id,
+            credentials.user_agent,
+            ['some-scope', 'some-other-scope'])
+
+        # Save credentials
+        store.put(credentials)
+        credentials = store.get()
+
+        self.assertNotEquals(None, credentials)
+        self.assertEquals('foo', credentials.access_token)
+
+        # Delete credentials
+        store.delete()
+        credentials = store.get()
+
+        self.assertEquals(None, credentials)
+
+        if os.name == 'posix':  # pragma: NO COVER
+            self.assertEquals(
+                0o600, stat.S_IMODE(os.stat(FILENAME).st_mode))
+
+    def test_multistore_file_custom_key(self):
+        credentials = self._create_test_credentials()
+
+        custom_key = {'myapp': 'testing', 'clientid': 'some client'}
+        store = multistore_file.get_credential_storage_custom_key(
+            FILENAME, custom_key)
+
+        store.put(credentials)
+        stored_credentials = store.get()
+
+        self.assertNotEquals(None, stored_credentials)
+        self.assertEqual(credentials.access_token,
+                         stored_credentials.access_token)
+
+        store.delete()
+        stored_credentials = store.get()
+
+        self.assertEquals(None, stored_credentials)
+
+    def test_multistore_file_custom_string_key(self):
+        credentials = self._create_test_credentials()
+
+        # store with string key
+        store = multistore_file.get_credential_storage_custom_string_key(
+            FILENAME, 'mykey')
+
+        store.put(credentials)
+        stored_credentials = store.get()
+
+        self.assertNotEquals(None, stored_credentials)
+        self.assertEqual(credentials.access_token,
+                         stored_credentials.access_token)
+
+        # try retrieving with a dictionary
+        multistore_file.get_credential_storage_custom_string_key(
+            FILENAME, {'key': 'mykey'})
+        stored_credentials = store.get()
+        self.assertNotEquals(None, stored_credentials)
+        self.assertEqual(credentials.access_token,
+                         stored_credentials.access_token)
+
+        store.delete()
+        stored_credentials = store.get()
+
+        self.assertEquals(None, stored_credentials)
+
+    def test_multistore_file_backwards_compatibility(self):
+        credentials = self._create_test_credentials()
+        scopes = ['scope1', 'scope2']
+
+        # store the credentials using the legacy key method
+        store = multistore_file.get_credential_storage(
+            FILENAME, 'client_id', 'user_agent', scopes)
+        store.put(credentials)
+
+        # retrieve the credentials using a custom key that matches the
+        # legacy key
+        key = {'clientId': 'client_id', 'userAgent': 'user_agent',
+               'scope': util.scopes_to_string(scopes)}
+        store = multistore_file.get_credential_storage_custom_key(
+            FILENAME, key)
+        stored_credentials = store.get()
+
+        self.assertEqual(credentials.access_token,
+                         stored_credentials.access_token)
+
+    def test_multistore_file_get_all_keys(self):
+        # start with no keys
+        keys = multistore_file.get_all_credential_keys(FILENAME)
+        self.assertEquals([], keys)
+
+        # store credentials
+        credentials = self._create_test_credentials(client_id='client1')
+        custom_key = {'myapp': 'testing', 'clientid': 'client1'}
+        store1 = multistore_file.get_credential_storage_custom_key(
+            FILENAME, custom_key)
+        store1.put(credentials)
+
+        keys = multistore_file.get_all_credential_keys(FILENAME)
+        self.assertEquals([custom_key], keys)
+
+        # store more credentials
+        credentials = self._create_test_credentials(client_id='client2')
+        string_key = 'string_key'
+        store2 = multistore_file.get_credential_storage_custom_string_key(
+            FILENAME, string_key)
+        store2.put(credentials)
+
+        keys = multistore_file.get_all_credential_keys(FILENAME)
+        self.assertEquals(2, len(keys))
+        self.assertTrue(custom_key in keys)
+        self.assertTrue({'key': string_key} in keys)
+
+        # back to no keys
+        store1.delete()
+        store2.delete()
+        keys = multistore_file.get_all_credential_keys(FILENAME)
+        self.assertEquals([], keys)
+
+    def _refresh_data_cache_helper(self):
+        multistore = multistore_file._MultiStore(FILENAME)
+        json_patch = mock.patch.object(multistore, '_locked_json_read')
+
+        return multistore, json_patch
+
+    def test__refresh_data_cache_bad_json(self):
+        multistore, json_patch = self._refresh_data_cache_helper()
+
+        with json_patch as json_mock:
+            json_mock.side_effect = ValueError('')
+            multistore._refresh_data_cache()
+            self.assertTrue(json_mock.called)
+            self.assertEqual(multistore._data, {})
+
+    def test__refresh_data_cache_bad_version(self):
+        multistore, json_patch = self._refresh_data_cache_helper()
+
+        with json_patch as json_mock:
+            json_mock.return_value = {}
+            multistore._refresh_data_cache()
+            self.assertTrue(json_mock.called)
+            self.assertEqual(multistore._data, {})
+
+    def test__refresh_data_cache_newer_version(self):
+        multistore, json_patch = self._refresh_data_cache_helper()
+
+        with json_patch as json_mock:
+            json_mock.return_value = {'file_version': 5}
+            with self.assertRaises(multistore_file.NewerCredentialStoreError):
+                multistore._refresh_data_cache()
+            self.assertTrue(json_mock.called)
+
+    def test__refresh_data_cache_bad_credentials(self):
+        multistore, json_patch = self._refresh_data_cache_helper()
+
+        with json_patch as json_mock:
+            json_mock.return_value = {
+                'file_version': 1,
+                'data': [
+                    {'lol': 'this is a bad credential object.'}
+                ]}
+            multistore._refresh_data_cache()
+            self.assertTrue(json_mock.called)
+            self.assertEqual(multistore._data, {})
+
+    def test__delete_credential_nonexistent(self):
+        multistore = multistore_file._MultiStore(FILENAME)
+
+        with mock.patch.object(multistore, '_write') as write_mock:
+            multistore._data = {}
+            multistore._delete_credential('nonexistent_key')
+            self.assertTrue(write_mock.called)
diff --git a/tests/contrib/test_sqlalchemy.py b/tests/contrib/test_sqlalchemy.py
new file mode 100644
index 0000000..421f516
--- /dev/null
+++ b/tests/contrib/test_sqlalchemy.py
@@ -0,0 +1,119 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import datetime
+
+import sqlalchemy
+import sqlalchemy.ext.declarative
+import sqlalchemy.orm
+import unittest2
+
+import oauth2client
+import oauth2client.client
+import oauth2client.contrib.sqlalchemy
+
+Base = sqlalchemy.ext.declarative.declarative_base()
+
+
+class DummyModel(Base):
+    __tablename__ = 'dummy'
+
+    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
+    # we will query against this, because of ROWID
+    key = sqlalchemy.Column(sqlalchemy.Integer)
+    credentials = sqlalchemy.Column(
+        oauth2client.contrib.sqlalchemy.CredentialsType)
+
+
+class TestSQLAlchemyStorage(unittest2.TestCase):
+    def setUp(self):
+        engine = sqlalchemy.create_engine('sqlite://')
+        Base.metadata.create_all(engine)
+
+        self.session = sqlalchemy.orm.sessionmaker(bind=engine)
+        self.credentials = oauth2client.client.OAuth2Credentials(
+            access_token='token',
+            client_id='client_id',
+            client_secret='client_secret',
+            refresh_token='refresh_token',
+            token_expiry=datetime.datetime.utcnow(),
+            token_uri=oauth2client.GOOGLE_TOKEN_URI,
+            user_agent='DummyAgent',
+        )
+
+    def tearDown(self):
+        session = self.session()
+        session.query(DummyModel).filter_by(key=1).delete()
+        session.commit()
+
+    def compare_credentials(self, result):
+        self.assertEqual(result.access_token, self.credentials.access_token)
+        self.assertEqual(result.client_id, self.credentials.client_id)
+        self.assertEqual(result.client_secret, self.credentials.client_secret)
+        self.assertEqual(result.refresh_token, self.credentials.refresh_token)
+        self.assertEqual(result.token_expiry, self.credentials.token_expiry)
+        self.assertEqual(result.token_uri, self.credentials.token_uri)
+        self.assertEqual(result.user_agent, self.credentials.user_agent)
+
+    def test_get(self):
+        session = self.session()
+        credentials_storage = oauth2client.contrib.sqlalchemy.Storage(
+            session=session,
+            model_class=DummyModel,
+            key_name='key',
+            key_value=1,
+            property_name='credentials',
+        )
+        self.assertIsNone(credentials_storage.get())
+        session.add(DummyModel(
+            key=1,
+            credentials=self.credentials,
+        ))
+        session.commit()
+
+        self.compare_credentials(credentials_storage.get())
+
+    def test_put(self):
+        session = self.session()
+        oauth2client.contrib.sqlalchemy.Storage(
+            session=session,
+            model_class=DummyModel,
+            key_name='key',
+            key_value=1,
+            property_name='credentials',
+        ).put(self.credentials)
+        session.commit()
+
+        entity = session.query(DummyModel).filter_by(key=1).first()
+        self.compare_credentials(entity.credentials)
+
+    def test_delete(self):
+        session = self.session()
+        session.add(DummyModel(
+            key=1,
+            credentials=self.credentials,
+        ))
+        session.commit()
+
+        query = session.query(DummyModel).filter_by(key=1)
+        self.assertIsNotNone(query.first())
+        oauth2client.contrib.sqlalchemy.Storage(
+            session=session,
+            model_class=DummyModel,
+            key_name='key',
+            key_value=1,
+            property_name='credentials',
+        ).delete()
+        session.commit()
+        self.assertIsNone(query.first())
diff --git a/tests/contrib/test_xsrfutil.py b/tests/contrib/test_xsrfutil.py
new file mode 100644
index 0000000..64b842f
--- /dev/null
+++ b/tests/contrib/test_xsrfutil.py
@@ -0,0 +1,293 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Tests for oauth2client.contrib.xsrfutil."""
+
+import base64
+
+import mock
+import unittest2
+
+from oauth2client import _helpers
+from oauth2client.contrib import xsrfutil
+
+# Jan 17 2008, 5:40PM
+TEST_KEY = b'test key'
+# Jan. 17, 2008 22:40:32.081230 UTC
+TEST_TIME = 1200609642081230
+TEST_USER_ID_1 = 123832983
+TEST_USER_ID_2 = 938297432
+TEST_ACTION_ID_1 = b'some_action'
+TEST_ACTION_ID_2 = b'some_other_action'
+TEST_EXTRA_INFO_1 = b'extra_info_1'
+TEST_EXTRA_INFO_2 = b'more_extra_info'
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class Test_generate_token(unittest2.TestCase):
+
+    def test_bad_positional(self):
+        # Need 2 positional arguments.
+        with self.assertRaises(TypeError):
+            xsrfutil.generate_token(None)
+        # At most 2 positional arguments.
+        with self.assertRaises(TypeError):
+            xsrfutil.generate_token(None, None, None)
+
+    def test_it(self):
+        digest = b'foobar'
+        digester = mock.MagicMock()
+        digester.digest = mock.MagicMock(name='digest', return_value=digest)
+        with mock.patch('oauth2client.contrib.xsrfutil.hmac') as hmac:
+            hmac.new = mock.MagicMock(name='new', return_value=digester)
+            token = xsrfutil.generate_token(TEST_KEY,
+                                            TEST_USER_ID_1,
+                                            action_id=TEST_ACTION_ID_1,
+                                            when=TEST_TIME)
+            hmac.new.assert_called_once_with(TEST_KEY)
+            digester.digest.assert_called_once_with()
+
+            expected_digest_calls = [
+                mock.call.update(_helpers._to_bytes(str(TEST_USER_ID_1))),
+                mock.call.update(xsrfutil.DELIMITER),
+                mock.call.update(TEST_ACTION_ID_1),
+                mock.call.update(xsrfutil.DELIMITER),
+                mock.call.update(_helpers._to_bytes(str(TEST_TIME))),
+            ]
+            self.assertEqual(digester.method_calls, expected_digest_calls)
+
+            expected_token_as_bytes = (digest + xsrfutil.DELIMITER +
+                                       _helpers._to_bytes(str(TEST_TIME)))
+            expected_token = base64.urlsafe_b64encode(
+                expected_token_as_bytes)
+            self.assertEqual(token, expected_token)
+
+    def test_with_system_time(self):
+        digest = b'foobar'
+        curr_time = 1440449755.74
+        digester = mock.MagicMock()
+        digester.digest = mock.MagicMock(name='digest', return_value=digest)
+        with mock.patch('oauth2client.contrib.xsrfutil.hmac') as hmac:
+            hmac.new = mock.MagicMock(name='new', return_value=digester)
+
+            with mock.patch('oauth2client.contrib.xsrfutil.time') as time:
+                time.time = mock.MagicMock(name='time', return_value=curr_time)
+                # when= is omitted
+                token = xsrfutil.generate_token(TEST_KEY,
+                                                TEST_USER_ID_1,
+                                                action_id=TEST_ACTION_ID_1)
+
+                hmac.new.assert_called_once_with(TEST_KEY)
+                time.time.assert_called_once_with()
+                digester.digest.assert_called_once_with()
+
+                expected_digest_calls = [
+                    mock.call.update(_helpers._to_bytes(str(TEST_USER_ID_1))),
+                    mock.call.update(xsrfutil.DELIMITER),
+                    mock.call.update(TEST_ACTION_ID_1),
+                    mock.call.update(xsrfutil.DELIMITER),
+                    mock.call.update(_helpers._to_bytes(str(int(curr_time)))),
+                ]
+                self.assertEqual(digester.method_calls, expected_digest_calls)
+
+                expected_token_as_bytes = (
+                    digest + xsrfutil.DELIMITER +
+                    _helpers._to_bytes(str(int(curr_time))))
+                expected_token = base64.urlsafe_b64encode(
+                    expected_token_as_bytes)
+                self.assertEqual(token, expected_token)
+
+
+class Test_validate_token(unittest2.TestCase):
+
+    def test_bad_positional(self):
+        # Need 3 positional arguments.
+        with self.assertRaises(TypeError):
+            xsrfutil.validate_token(None, None)
+        # At most 3 positional arguments.
+        with self.assertRaises(TypeError):
+            xsrfutil.validate_token(None, None, None, None)
+
+    def test_no_token(self):
+        key = token = user_id = None
+        self.assertFalse(xsrfutil.validate_token(key, token, user_id))
+
+    def test_token_not_valid_base64(self):
+        key = user_id = None
+        token = b'a'  # Bad padding
+        self.assertFalse(xsrfutil.validate_token(key, token, user_id))
+
+    def test_token_non_integer(self):
+        key = user_id = None
+        token = base64.b64encode(b'abc' + xsrfutil.DELIMITER + b'xyz')
+        self.assertFalse(xsrfutil.validate_token(key, token, user_id))
+
+    def test_token_too_old_implicit_current_time(self):
+        token_time = 123456789
+        curr_time = token_time + xsrfutil.DEFAULT_TIMEOUT_SECS + 1
+
+        key = user_id = None
+        token = base64.b64encode(_helpers._to_bytes(str(token_time)))
+        with mock.patch('oauth2client.contrib.xsrfutil.time') as time:
+            time.time = mock.MagicMock(name='time', return_value=curr_time)
+            self.assertFalse(xsrfutil.validate_token(key, token, user_id))
+            time.time.assert_called_once_with()
+
+    def test_token_too_old_explicit_current_time(self):
+        token_time = 123456789
+        curr_time = token_time + xsrfutil.DEFAULT_TIMEOUT_SECS + 1
+
+        key = user_id = None
+        token = base64.b64encode(_helpers._to_bytes(str(token_time)))
+        self.assertFalse(xsrfutil.validate_token(key, token, user_id,
+                                                 current_time=curr_time))
+
+    def test_token_length_differs_from_generated(self):
+        token_time = 123456789
+        # Make sure it isn't too old.
+        curr_time = token_time + xsrfutil.DEFAULT_TIMEOUT_SECS - 1
+
+        key = object()
+        user_id = object()
+        action_id = object()
+        token = base64.b64encode(_helpers._to_bytes(str(token_time)))
+        generated_token = b'a'
+        # Make sure the token length comparison will fail.
+        self.assertNotEqual(len(token), len(generated_token))
+
+        with mock.patch('oauth2client.contrib.xsrfutil.generate_token',
+                        return_value=generated_token) as gen_tok:
+            self.assertFalse(xsrfutil.validate_token(key, token, user_id,
+                                                     current_time=curr_time,
+                                                     action_id=action_id))
+            gen_tok.assert_called_once_with(key, user_id, action_id=action_id,
+                                            when=token_time)
+
+    def test_token_differs_from_generated_but_same_length(self):
+        token_time = 123456789
+        # Make sure it isn't too old.
+        curr_time = token_time + xsrfutil.DEFAULT_TIMEOUT_SECS - 1
+
+        key = object()
+        user_id = object()
+        action_id = object()
+        token = base64.b64encode(_helpers._to_bytes(str(token_time)))
+        # It is encoded as b'MTIzNDU2Nzg5', which has length 12.
+        generated_token = b'M' * 12
+        # Make sure the token length comparison will succeed, but the token
+        # comparison will fail.
+        self.assertEqual(len(token), len(generated_token))
+        self.assertNotEqual(token, generated_token)
+
+        with mock.patch('oauth2client.contrib.xsrfutil.generate_token',
+                        return_value=generated_token) as gen_tok:
+            self.assertFalse(xsrfutil.validate_token(key, token, user_id,
+                                                     current_time=curr_time,
+                                                     action_id=action_id))
+            gen_tok.assert_called_once_with(key, user_id, action_id=action_id,
+                                            when=token_time)
+
+    def test_success(self):
+        token_time = 123456789
+        # Make sure it isn't too old.
+        curr_time = token_time + xsrfutil.DEFAULT_TIMEOUT_SECS - 1
+
+        key = object()
+        user_id = object()
+        action_id = object()
+        token = base64.b64encode(_helpers._to_bytes(str(token_time)))
+        with mock.patch('oauth2client.contrib.xsrfutil.generate_token',
+                        return_value=token) as gen_tok:
+            self.assertTrue(xsrfutil.validate_token(key, token, user_id,
+                                                    current_time=curr_time,
+                                                    action_id=action_id))
+            gen_tok.assert_called_once_with(key, user_id, action_id=action_id,
+                                            when=token_time)
+
+
+class XsrfUtilTests(unittest2.TestCase):
+    """Test xsrfutil functions."""
+
+    def testGenerateAndValidateToken(self):
+        """Test generating and validating a token."""
+        token = xsrfutil.generate_token(TEST_KEY,
+                                        TEST_USER_ID_1,
+                                        action_id=TEST_ACTION_ID_1,
+                                        when=TEST_TIME)
+
+        # Check that the token is considered valid when it should be.
+        self.assertTrue(xsrfutil.validate_token(TEST_KEY,
+                                                token,
+                                                TEST_USER_ID_1,
+                                                action_id=TEST_ACTION_ID_1,
+                                                current_time=TEST_TIME))
+
+        # Should still be valid 15 minutes later.
+        later15mins = TEST_TIME + 15 * 60
+        self.assertTrue(xsrfutil.validate_token(TEST_KEY,
+                                                token,
+                                                TEST_USER_ID_1,
+                                                action_id=TEST_ACTION_ID_1,
+                                                current_time=later15mins))
+
+        # But not if beyond the timeout.
+        later2hours = TEST_TIME + 2 * 60 * 60
+        self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                                 token,
+                                                 TEST_USER_ID_1,
+                                                 action_id=TEST_ACTION_ID_1,
+                                                 current_time=later2hours))
+
+        # Or if the key is different.
+        self.assertFalse(xsrfutil.validate_token('another key',
+                                                 token,
+                                                 TEST_USER_ID_1,
+                                                 action_id=TEST_ACTION_ID_1,
+                                                 current_time=later15mins))
+
+        # Or the user ID....
+        self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                                 token,
+                                                 TEST_USER_ID_2,
+                                                 action_id=TEST_ACTION_ID_1,
+                                                 current_time=later15mins))
+
+        # Or the action ID...
+        self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                                 token,
+                                                 TEST_USER_ID_1,
+                                                 action_id=TEST_ACTION_ID_2,
+                                                 current_time=later15mins))
+
+        # Invalid when truncated
+        self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                                 token[:-1],
+                                                 TEST_USER_ID_1,
+                                                 action_id=TEST_ACTION_ID_1,
+                                                 current_time=later15mins))
+
+        # Invalid with extra garbage
+        self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                                 token + b'x',
+                                                 TEST_USER_ID_1,
+                                                 action_id=TEST_ACTION_ID_1,
+                                                 current_time=later15mins))
+
+        # Invalid with token of None
+        self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                                 None,
+                                                 TEST_USER_ID_1,
+                                                 action_id=TEST_ACTION_ID_1))
diff --git a/tests/data/app.yaml b/tests/data/app.yaml
new file mode 100644
index 0000000..9268dfa
--- /dev/null
+++ b/tests/data/app.yaml
@@ -0,0 +1,10 @@
+# Dummy app.yaml to placate nosegae.
+application: oauth2client
+version: 1
+runtime: python27
+api_version: 1
+threadsafe: yes
+
+handlers:
+- url: /.*
+  script: null.app
diff --git a/tests/data/certs.json b/tests/data/certs.json
new file mode 100644
index 0000000..fa09416
--- /dev/null
+++ b/tests/data/certs.json
@@ -0,0 +1,3 @@
+{
+"foo": "-----BEGIN CERTIFICATE-----\r\nMIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV\r\nBAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV\r\nMRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\r\nCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM\r\n7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer\r\nuQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp\r\ngyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4\r\n+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3\r\nZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O\r\ngN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh\r\nGaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD\r\nAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr\r\nodJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk\r\n+JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9\r\novNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql\r\nybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT\r\ncDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB\r\n-----END CERTIFICATE-----\r\n"
+}
diff --git a/tests/data/client_secrets.json b/tests/data/client_secrets.json
new file mode 100644
index 0000000..5356103
--- /dev/null
+++ b/tests/data/client_secrets.json
@@ -0,0 +1,10 @@
+{
+  "web": {
+    "client_id": "foo_client_id",
+    "client_secret": "foo_client_secret",
+    "redirect_uris": [],
+    "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
+    "token_uri": "https://www.googleapis.com/oauth2/v4/token",
+    "revoke_uri": "https://accounts.google.com/o/oauth2/revoke"
+  }
+}
diff --git a/tests/data/gcloud/application_default_credentials.json b/tests/data/gcloud/application_default_credentials.json
new file mode 100644
index 0000000..f011013
--- /dev/null
+++ b/tests/data/gcloud/application_default_credentials.json
@@ -0,0 +1,7 @@
+{
+  "private_key_id": "ABCDEF",
+  "private_key": "Bag Attributes\n    friendlyName: key\n    localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n",
+  "client_email": "dummy@google.com",
+  "client_id": "123",
+  "type": "service_account"
+}
diff --git a/tests/data/gcloud/application_default_credentials_authorized_user.json b/tests/data/gcloud/application_default_credentials_authorized_user.json
new file mode 100644
index 0000000..4787ace
--- /dev/null
+++ b/tests/data/gcloud/application_default_credentials_authorized_user.json
@@ -0,0 +1,6 @@
+{
+  "client_id": "123",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "type": "authorized_user"
+}
diff --git a/tests/data/gcloud/application_default_credentials_malformed_1.json b/tests/data/gcloud/application_default_credentials_malformed_1.json
new file mode 100644
index 0000000..fe9fd1e
--- /dev/null
+++ b/tests/data/gcloud/application_default_credentials_malformed_1.json
@@ -0,0 +1,9 @@
+{
+  "type": "serviceaccount",
+  "client_id": "123",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "client_email": "dummy@google.com",
+  "private_key_id": "ABCDEF",
+  "private_key": "Bag Attributes\n    friendlyName: key\n    localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
+}
\ No newline at end of file
diff --git a/tests/data/gcloud/application_default_credentials_malformed_2.json b/tests/data/gcloud/application_default_credentials_malformed_2.json
new file mode 100644
index 0000000..6f1ae52
--- /dev/null
+++ b/tests/data/gcloud/application_default_credentials_malformed_2.json
@@ -0,0 +1,8 @@
+{
+  "type": "service_account",
+  "client_id": "123",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "client_email": "dummy@google.com",
+  "private_key": "Bag Attributes\n    friendlyName: key\n    localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
+}
\ No newline at end of file
diff --git a/tests/data/gcloud/application_default_credentials_malformed_3.json b/tests/data/gcloud/application_default_credentials_malformed_3.json
new file mode 100644
index 0000000..efed137
--- /dev/null
+++ b/tests/data/gcloud/application_default_credentials_malformed_3.json
@@ -0,0 +1,9 @@
+{
+  "type": "service_account"
+  "client_id": "123",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "client_email": "dummy@google.com",
+  "private_key_id": "ABCDEF",
+  "private_key": "Bag Attributes\n    friendlyName: key\n    localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
+}
\ No newline at end of file
diff --git a/tests/data/key.json.enc b/tests/data/key.json.enc
new file mode 100644
index 0000000..1cf0705
--- /dev/null
+++ b/tests/data/key.json.enc
Binary files differ
diff --git a/tests/data/key.p12.enc b/tests/data/key.p12.enc
new file mode 100644
index 0000000..5e2f6ec
--- /dev/null
+++ b/tests/data/key.p12.enc
Binary files differ
diff --git a/tests/data/pem_from_pkcs12.pem b/tests/data/pem_from_pkcs12.pem
new file mode 100644
index 0000000..2d77e10
--- /dev/null
+++ b/tests/data/pem_from_pkcs12.pem
@@ -0,0 +1,32 @@
+Bag Attributes
+    friendlyName: key
+    localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC 
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi
+tUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p
+oJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR
+aIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt
+w21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE
+GKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp
++qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN
+TzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4
+QoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG
+Dy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo
+f1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR
++DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p
+IwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a
+c3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7
+SgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0
+jGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY
+iuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5
+sdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO
+GCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk
+Brn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk
+t7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2
+DwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS
+LZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB
+WGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa
+XUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB
+VL5h7N0VstYhGgycuPpcIUQa
+-----END PRIVATE KEY-----
diff --git a/tests/data/pem_from_pkcs12_alternate.pem b/tests/data/pem_from_pkcs12_alternate.pem
new file mode 100644
index 0000000..5744354
--- /dev/null
+++ b/tests/data/pem_from_pkcs12_alternate.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
+7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
+xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
+SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
+pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
+SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
+nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
+HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
+nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
+IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
+YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
+Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
+vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
+B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
+aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
+eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
+aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
+klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
+CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
+UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
+soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
+bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
+504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
+YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
+BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/data/privatekey.p12 b/tests/data/privatekey.p12
new file mode 100644
index 0000000..c369ecb
--- /dev/null
+++ b/tests/data/privatekey.p12
Binary files differ
diff --git a/tests/data/privatekey.pem b/tests/data/privatekey.pem
new file mode 100644
index 0000000..5744354
--- /dev/null
+++ b/tests/data/privatekey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
+7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
+xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
+SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
+pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
+SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
+nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
+HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
+nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
+IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
+YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
+Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
+vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
+B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
+aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
+eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
+aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
+klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
+CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
+UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
+soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
+bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
+504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
+YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
+BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/data/privatekey.pub b/tests/data/privatekey.pub
new file mode 100644
index 0000000..11fdaa4
--- /dev/null
+++ b/tests/data/privatekey.pub
@@ -0,0 +1,8 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+kdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU
+1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS
+5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+z
+pyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc/
+/fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+-----END RSA PUBLIC KEY-----
diff --git a/tests/data/public_cert.pem b/tests/data/public_cert.pem
new file mode 100644
index 0000000..7af6ca3
--- /dev/null
+++ b/tests/data/public_cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
+BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
+MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
+7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
+uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
+gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
+ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
+gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
+GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
+AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
+odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
+ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
+ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
+cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
+-----END CERTIFICATE-----
diff --git a/tests/data/publickey_openssl.pem b/tests/data/publickey_openssl.pem
new file mode 100644
index 0000000..893ee79
--- /dev/null
+++ b/tests/data/publickey_openssl.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9R
+N4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7
+K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCa
+kXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7
+hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7q
+iouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5O
+YQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tests/data/unfilled_client_secrets.json b/tests/data/unfilled_client_secrets.json
new file mode 100644
index 0000000..a85ca01
--- /dev/null
+++ b/tests/data/unfilled_client_secrets.json
@@ -0,0 +1,9 @@
+{
+  "web": {
+    "client_id": "[[INSERT CLIENT ID HERE]]",
+    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
+    "redirect_uris": [],
+    "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
+    "token_uri": "https://www.googleapis.com/oauth2/v4/token"
+  }
+}
diff --git a/tests/data/user-key.json.enc b/tests/data/user-key.json.enc
new file mode 100644
index 0000000..03e1bc6
--- /dev/null
+++ b/tests/data/user-key.json.enc
Binary files differ
diff --git a/tests/http_mock.py b/tests/http_mock.py
new file mode 100644
index 0000000..6053299
--- /dev/null
+++ b/tests/http_mock.py
@@ -0,0 +1,112 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Copy of googleapiclient.http's mock functionality."""
+
+import httplib2
+
+# TODO(craigcitro): Find a cleaner way to share this code with googleapiclient.
+
+
+class HttpMock(object):
+    """Mock of httplib2.Http"""
+
+    def __init__(self, headers=None):
+        """HttpMock constructor.
+
+        Args:
+            headers: dict, header to return with response
+        """
+        if headers is None:
+            headers = {'status': '200'}
+        self.data = None
+        self.response_headers = headers
+        self.headers = None
+        self.uri = None
+        self.method = None
+        self.body = None
+        self.headers = None
+
+    def request(self, uri,
+                method='GET',
+                body=None,
+                headers=None,
+                redirections=1,
+                connection_type=None):
+        self.uri = uri
+        self.method = method
+        self.body = body
+        self.headers = headers
+        return httplib2.Response(self.response_headers), self.data
+
+
+class HttpMockSequence(object):
+    """Mock of httplib2.Http
+
+    Mocks a sequence of calls to request returning different responses for each
+    call. Create an instance initialized with the desired response headers
+    and content and then use as if an httplib2.Http instance::
+
+        http = HttpMockSequence([
+            ({'status': '401'}, b''),
+            ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
+            ({'status': '200'}, 'echo_request_headers'),
+        ])
+        resp, content = http.request("http://examples.com")
+
+    There are special values you can pass in for content to trigger
+    behavours that are helpful in testing.
+
+    * 'echo_request_headers' means return the request headers in the response
+       body
+    * 'echo_request_body' means return the request body in the response body
+    """
+
+    def __init__(self, iterable):
+        """HttpMockSequence constructor.
+
+        Args:
+            iterable: iterable, a sequence of pairs of (headers, body)
+        """
+        self._iterable = iterable
+        self.follow_redirects = True
+        self.requests = []
+
+    def request(self, uri,
+                method='GET',
+                body=None,
+                headers=None,
+                redirections=1,
+                connection_type=None):
+        resp, content = self._iterable.pop(0)
+        self.requests.append({'uri': uri, 'body': body, 'headers': headers})
+        # Read any underlying stream before sending the request.
+        body_stream_content = (body.read()
+                               if getattr(body, 'read', None) else None)
+        if content == 'echo_request_headers':
+            content = headers
+        elif content == 'echo_request_body':
+            content = (body
+                       if body_stream_content is None else body_stream_content)
+        return httplib2.Response(resp), content
+
+
+class CacheMock(object):
+
+    def __init__(self):
+        self.cache = {}
+
+    def get(self, key, namespace=''):
+        # ignoring namespace for easier testing
+        return self.cache.get(key, None)
diff --git a/tests/test__helpers.py b/tests/test__helpers.py
new file mode 100644
index 0000000..cd54186
--- /dev/null
+++ b/tests/test__helpers.py
@@ -0,0 +1,114 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+"""Unit tests for oauth2client._helpers."""
+
+import unittest2
+
+from oauth2client import _helpers
+
+
+class Test__parse_pem_key(unittest2.TestCase):
+
+    def test_valid_input(self):
+        test_string = b'1234-----BEGIN FOO BAR BAZ'
+        result = _helpers._parse_pem_key(test_string)
+        self.assertEqual(result, test_string[4:])
+
+    def test_bad_input(self):
+        test_string = b'DOES NOT HAVE DASHES'
+        result = _helpers._parse_pem_key(test_string)
+        self.assertEqual(result, None)
+
+
+class Test__json_encode(unittest2.TestCase):
+
+    def test_dictionary_input(self):
+        # Use only a single key since dictionary hash order
+        # is non-deterministic.
+        data = {u'foo': 10}
+        result = _helpers._json_encode(data)
+        self.assertEqual(result, '{"foo":10}')
+
+    def test_list_input(self):
+        data = [42, 1337]
+        result = _helpers._json_encode(data)
+        self.assertEqual(result, '[42,1337]')
+
+
+class Test__to_bytes(unittest2.TestCase):
+
+    def test_with_bytes(self):
+        value = b'bytes-val'
+        self.assertEqual(_helpers._to_bytes(value), value)
+
+    def test_with_unicode(self):
+        value = u'string-val'
+        encoded_value = b'string-val'
+        self.assertEqual(_helpers._to_bytes(value), encoded_value)
+
+    def test_with_nonstring_type(self):
+        value = object()
+        with self.assertRaises(ValueError):
+            _helpers._to_bytes(value)
+
+
+class Test__from_bytes(unittest2.TestCase):
+
+    def test_with_unicode(self):
+        value = u'bytes-val'
+        self.assertEqual(_helpers._from_bytes(value), value)
+
+    def test_with_bytes(self):
+        value = b'string-val'
+        decoded_value = u'string-val'
+        self.assertEqual(_helpers._from_bytes(value), decoded_value)
+
+    def test_with_nonstring_type(self):
+        value = object()
+        with self.assertRaises(ValueError):
+            _helpers._from_bytes(value)
+
+
+class Test__urlsafe_b64encode(unittest2.TestCase):
+
+    DEADBEEF_ENCODED = b'ZGVhZGJlZWY'
+
+    def test_valid_input_bytes(self):
+        test_string = b'deadbeef'
+        result = _helpers._urlsafe_b64encode(test_string)
+        self.assertEqual(result, self.DEADBEEF_ENCODED)
+
+    def test_valid_input_unicode(self):
+        test_string = u'deadbeef'
+        result = _helpers._urlsafe_b64encode(test_string)
+        self.assertEqual(result, self.DEADBEEF_ENCODED)
+
+
+class Test__urlsafe_b64decode(unittest2.TestCase):
+
+    def test_valid_input_bytes(self):
+        test_string = b'ZGVhZGJlZWY'
+        result = _helpers._urlsafe_b64decode(test_string)
+        self.assertEqual(result, b'deadbeef')
+
+    def test_valid_input_unicode(self):
+        test_string = b'ZGVhZGJlZWY'
+        result = _helpers._urlsafe_b64decode(test_string)
+        self.assertEqual(result, b'deadbeef')
+
+    def test_bad_input(self):
+        import binascii
+        bad_string = b'+'
+        with self.assertRaises((TypeError, binascii.Error)):
+            _helpers._urlsafe_b64decode(bad_string)
diff --git a/tests/test__pure_python_crypt.py b/tests/test__pure_python_crypt.py
new file mode 100644
index 0000000..3c2962a
--- /dev/null
+++ b/tests/test__pure_python_crypt.py
@@ -0,0 +1,183 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for oauth2client._pure_python_crypt."""
+
+import os
+
+import mock
+from pyasn1_modules import pem
+import rsa
+import six
+import unittest2
+
+from oauth2client import _helpers
+from oauth2client import _pure_python_crypt
+from oauth2client import crypt
+
+
+class TestRsaVerifier(unittest2.TestCase):
+
+    PUBLIC_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
+                                       'data', 'privatekey.pub')
+    PUBLIC_CERT_FILENAME = os.path.join(os.path.dirname(__file__),
+                                        'data', 'public_cert.pem')
+    PRIVATE_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
+                                        'data', 'privatekey.pem')
+
+    def _load_public_key_bytes(self):
+        with open(self.PUBLIC_KEY_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def _load_public_cert_bytes(self):
+        with open(self.PUBLIC_CERT_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def _load_private_key_bytes(self):
+        with open(self.PRIVATE_KEY_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def test_verify_success(self):
+        to_sign = b'foo'
+        signer = crypt.RsaSigner.from_string(self._load_private_key_bytes())
+        actual_signature = signer.sign(to_sign)
+
+        verifier = crypt.RsaVerifier.from_string(
+            self._load_public_key_bytes(), is_x509_cert=False)
+        self.assertTrue(verifier.verify(to_sign, actual_signature))
+
+    def test_verify_unicode_success(self):
+        to_sign = u'foo'
+        signer = crypt.RsaSigner.from_string(self._load_private_key_bytes())
+        actual_signature = signer.sign(to_sign)
+
+        verifier = crypt.RsaVerifier.from_string(
+            self._load_public_key_bytes(), is_x509_cert=False)
+        self.assertTrue(verifier.verify(to_sign, actual_signature))
+
+    def test_verify_failure(self):
+        verifier = crypt.RsaVerifier.from_string(
+            self._load_public_key_bytes(), is_x509_cert=False)
+        bad_signature1 = b''
+        self.assertFalse(verifier.verify(b'foo', bad_signature1))
+        bad_signature2 = b'a'
+        self.assertFalse(verifier.verify(b'foo', bad_signature2))
+
+    def test_from_string_pub_key(self):
+        public_key = self._load_public_key_bytes()
+        verifier = crypt.RsaVerifier.from_string(
+            public_key, is_x509_cert=False)
+        self.assertIsInstance(verifier, crypt.RsaVerifier)
+        self.assertIsInstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_key_unicode(self):
+        public_key = _helpers._from_bytes(self._load_public_key_bytes())
+        verifier = crypt.RsaVerifier.from_string(
+            public_key, is_x509_cert=False)
+        self.assertIsInstance(verifier, crypt.RsaVerifier)
+        self.assertIsInstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_cert(self):
+        public_cert = self._load_public_cert_bytes()
+        verifier = crypt.RsaVerifier.from_string(
+            public_cert, is_x509_cert=True)
+        self.assertIsInstance(verifier, crypt.RsaVerifier)
+        self.assertIsInstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_cert_unicode(self):
+        public_cert = _helpers._from_bytes(self._load_public_cert_bytes())
+        verifier = crypt.RsaVerifier.from_string(
+            public_cert, is_x509_cert=True)
+        self.assertIsInstance(verifier, crypt.RsaVerifier)
+        self.assertIsInstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_cert_failure(self):
+        cert_bytes = self._load_public_cert_bytes()
+        true_der = rsa.pem.load_pem(cert_bytes, 'CERTIFICATE')
+        with mock.patch('rsa.pem.load_pem',
+                        return_value=true_der + b'extra') as load_pem:
+            with self.assertRaises(ValueError):
+                crypt.RsaVerifier.from_string(cert_bytes, is_x509_cert=True)
+            load_pem.assert_called_once_with(cert_bytes, 'CERTIFICATE')
+
+
+class TestRsaSigner(unittest2.TestCase):
+
+    PKCS1_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
+                                      'data', 'privatekey.pem')
+    PKCS8_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
+                                      'data', 'pem_from_pkcs12.pem')
+    PKCS12_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
+                                       'data', 'privatekey.p12')
+
+    def _load_pkcs1_key_bytes(self):
+        with open(self.PKCS1_KEY_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def _load_pkcs8_key_bytes(self):
+        with open(self.PKCS8_KEY_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def _load_pkcs12_key_bytes(self):
+        with open(self.PKCS12_KEY_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def test_from_string_pkcs1(self):
+        key_bytes = self._load_pkcs1_key_bytes()
+        signer = crypt.RsaSigner.from_string(key_bytes)
+        self.assertIsInstance(signer, crypt.RsaSigner)
+        self.assertIsInstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs1_unicode(self):
+        key_bytes = _helpers._from_bytes(self._load_pkcs1_key_bytes())
+        signer = crypt.RsaSigner.from_string(key_bytes)
+        self.assertIsInstance(signer, crypt.RsaSigner)
+        self.assertIsInstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs8(self):
+        key_bytes = self._load_pkcs8_key_bytes()
+        signer = crypt.RsaSigner.from_string(key_bytes)
+        self.assertIsInstance(signer, crypt.RsaSigner)
+        self.assertIsInstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs8_extra_bytes(self):
+        key_bytes = self._load_pkcs8_key_bytes()
+        _, pem_bytes = pem.readPemBlocksFromFile(
+            six.StringIO(_helpers._from_bytes(key_bytes)),
+            _pure_python_crypt._PKCS8_MARKER)
+
+        with mock.patch('pyasn1.codec.der.decoder.decode') as mock_decode:
+            key_info, remaining = None, 'extra'
+            mock_decode.return_value = (key_info, remaining)
+            with self.assertRaises(ValueError):
+                crypt.RsaSigner.from_string(key_bytes)
+            # Verify mock was called.
+            mock_decode.assert_called_once_with(
+                pem_bytes, asn1Spec=_pure_python_crypt._PKCS8_SPEC)
+
+    def test_from_string_pkcs8_unicode(self):
+        key_bytes = _helpers._from_bytes(self._load_pkcs8_key_bytes())
+        signer = crypt.RsaSigner.from_string(key_bytes)
+        self.assertIsInstance(signer, crypt.RsaSigner)
+        self.assertIsInstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs12(self):
+        key_bytes = self._load_pkcs12_key_bytes()
+        with self.assertRaises(ValueError):
+            crypt.RsaSigner.from_string(key_bytes)
+
+    def test_from_string_bogus_key(self):
+        key_bytes = 'bogus-key'
+        with self.assertRaises(ValueError):
+            crypt.RsaSigner.from_string(key_bytes)
diff --git a/tests/test__pycrypto_crypt.py b/tests/test__pycrypto_crypt.py
new file mode 100644
index 0000000..2f45291
--- /dev/null
+++ b/tests/test__pycrypto_crypt.py
@@ -0,0 +1,73 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+"""Unit tests for oauth2client._pycrypto_crypt."""
+
+import os
+
+import unittest2
+
+from oauth2client import crypt
+
+
+class TestPyCryptoVerifier(unittest2.TestCase):
+
+    PUBLIC_CERT_FILENAME = os.path.join(os.path.dirname(__file__),
+                                        'data', 'public_cert.pem')
+    PRIVATE_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
+                                        'data', 'privatekey.pem')
+
+    def _load_public_cert_bytes(self):
+        with open(self.PUBLIC_CERT_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def _load_private_key_bytes(self):
+        with open(self.PRIVATE_KEY_FILENAME, 'rb') as fh:
+            return fh.read()
+
+    def test_verify_success(self):
+        to_sign = b'foo'
+        signer = crypt.PyCryptoSigner.from_string(
+            self._load_private_key_bytes())
+        actual_signature = signer.sign(to_sign)
+
+        verifier = crypt.PyCryptoVerifier.from_string(
+            self._load_public_cert_bytes(), is_x509_cert=True)
+        self.assertTrue(verifier.verify(to_sign, actual_signature))
+
+    def test_verify_failure(self):
+        verifier = crypt.PyCryptoVerifier.from_string(
+            self._load_public_cert_bytes(), is_x509_cert=True)
+        bad_signature = b''
+        self.assertFalse(verifier.verify(b'foo', bad_signature))
+
+    def test_verify_bad_key(self):
+        verifier = crypt.PyCryptoVerifier.from_string(
+            self._load_public_cert_bytes(), is_x509_cert=True)
+        bad_signature = b''
+        self.assertFalse(verifier.verify(b'foo', bad_signature))
+
+    def test_from_string_unicode_key(self):
+        public_key = self._load_public_cert_bytes()
+        public_key = public_key.decode('utf-8')
+        verifier = crypt.PyCryptoVerifier.from_string(
+            public_key, is_x509_cert=True)
+        self.assertIsInstance(verifier, crypt.PyCryptoVerifier)
+
+
+class TestPyCryptoSigner(unittest2.TestCase):
+
+    def test_from_string_bad_key(self):
+        key_bytes = 'definitely-not-pem-format'
+        with self.assertRaises(NotImplementedError):
+            crypt.PyCryptoSigner.from_string(key_bytes)
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 0000000..db75603
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,2358 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Oauth2client tests
+
+Unit tests for oauth2client.
+"""
+
+import base64
+import contextlib
+import copy
+import datetime
+import json
+import os
+import socket
+import sys
+import tempfile
+
+import httplib2
+import mock
+import six
+from six.moves import http_client
+from six.moves import urllib
+import unittest2
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client import service_account
+from oauth2client import util
+from .http_mock import CacheMock
+from .http_mock import HttpMock
+from .http_mock import HttpMockSequence
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+
+
+# TODO(craigcitro): This is duplicated from
+# googleapiclient.test_discovery; consolidate these definitions.
+def assertUrisEqual(testcase, expected, actual):
+    """Test that URIs are the same, up to reordering of query parameters."""
+    expected = urllib.parse.urlparse(expected)
+    actual = urllib.parse.urlparse(actual)
+    testcase.assertEqual(expected.scheme, actual.scheme)
+    testcase.assertEqual(expected.netloc, actual.netloc)
+    testcase.assertEqual(expected.path, actual.path)
+    testcase.assertEqual(expected.params, actual.params)
+    testcase.assertEqual(expected.fragment, actual.fragment)
+    expected_query = urllib.parse.parse_qs(expected.query)
+    actual_query = urllib.parse.parse_qs(actual.query)
+    for name in expected_query.keys():
+        testcase.assertEqual(expected_query[name], actual_query[name])
+    for name in actual_query.keys():
+        testcase.assertEqual(expected_query[name], actual_query[name])
+
+
+def datafile(filename):
+    return os.path.join(DATA_DIR, filename)
+
+
+def load_and_cache(existing_file, fakename, cache_mock):
+    client_type, client_info = clientsecrets._loadfile(datafile(existing_file))
+    cache_mock.cache[fakename] = {client_type: client_info}
+
+
+class CredentialsTests(unittest2.TestCase):
+
+    def test_to_from_json(self):
+        credentials = client.Credentials()
+        json = credentials.to_json()
+        client.Credentials.new_from_json(json)
+
+    def test_authorize_abstract(self):
+        credentials = client.Credentials()
+        http = object()
+        with self.assertRaises(NotImplementedError):
+            credentials.authorize(http)
+
+    def test_refresh_abstract(self):
+        credentials = client.Credentials()
+        http = object()
+        with self.assertRaises(NotImplementedError):
+            credentials.refresh(http)
+
+    def test_revoke_abstract(self):
+        credentials = client.Credentials()
+        http = object()
+        with self.assertRaises(NotImplementedError):
+            credentials.revoke(http)
+
+    def test_apply_abstract(self):
+        credentials = client.Credentials()
+        headers = {}
+        with self.assertRaises(NotImplementedError):
+            credentials.apply(headers)
+
+    def test__to_json_basic(self):
+        credentials = client.Credentials()
+        json_payload = credentials._to_json([])
+        # str(bytes) in Python2 and str(unicode) in Python3
+        self.assertIsInstance(json_payload, str)
+        payload = json.loads(json_payload)
+        expected_payload = {
+            '_class': client.Credentials.__name__,
+            '_module': client.Credentials.__module__,
+            'token_expiry': None,
+        }
+        self.assertEqual(payload, expected_payload)
+
+    def test__to_json_with_strip(self):
+        credentials = client.Credentials()
+        credentials.foo = 'bar'
+        credentials.baz = 'quux'
+        to_strip = ['foo']
+        json_payload = credentials._to_json(to_strip)
+        # str(bytes) in Python2 and str(unicode) in Python3
+        self.assertIsInstance(json_payload, str)
+        payload = json.loads(json_payload)
+        expected_payload = {
+            '_class': client.Credentials.__name__,
+            '_module': client.Credentials.__module__,
+            'token_expiry': None,
+            'baz': credentials.baz,
+        }
+        self.assertEqual(payload, expected_payload)
+
+    def test__to_json_to_serialize(self):
+        credentials = client.Credentials()
+        to_serialize = {
+            'foo': b'bar',
+            'baz': u'quux',
+            'st': set(['a', 'b']),
+        }
+        orig_vals = to_serialize.copy()
+        json_payload = credentials._to_json([], to_serialize=to_serialize)
+        # str(bytes) in Python2 and str(unicode) in Python3
+        self.assertIsInstance(json_payload, str)
+        payload = json.loads(json_payload)
+        expected_payload = {
+            '_class': client.Credentials.__name__,
+            '_module': client.Credentials.__module__,
+            'token_expiry': None,
+        }
+        expected_payload.update(to_serialize)
+        # Special-case the set.
+        expected_payload['st'] = list(expected_payload['st'])
+        # Special-case the bytes.
+        expected_payload['foo'] = u'bar'
+        self.assertEqual(payload, expected_payload)
+        # Make sure the method call didn't modify our dictionary.
+        self.assertEqual(to_serialize, orig_vals)
+
+    @mock.patch.object(client.Credentials, '_to_json',
+                       return_value=object())
+    def test_to_json(self, to_json):
+        credentials = client.Credentials()
+        self.assertEqual(credentials.to_json(), to_json.return_value)
+        to_json.assert_called_once_with(
+            client.Credentials.NON_SERIALIZED_MEMBERS)
+
+    def test_new_from_json_no_data(self):
+        creds_data = {}
+        json_data = json.dumps(creds_data)
+        with self.assertRaises(KeyError):
+            client.Credentials.new_from_json(json_data)
+
+    def test_new_from_json_basic_data(self):
+        creds_data = {
+            '_module': 'oauth2client.client',
+            '_class': 'Credentials',
+        }
+        json_data = json.dumps(creds_data)
+        credentials = client.Credentials.new_from_json(json_data)
+        self.assertIsInstance(credentials, client.Credentials)
+
+    def test_new_from_json_old_name(self):
+        creds_data = {
+            '_module': 'oauth2client.googleapiclient.client',
+            '_class': 'Credentials',
+        }
+        json_data = json.dumps(creds_data)
+        credentials = client.Credentials.new_from_json(json_data)
+        self.assertIsInstance(credentials, client.Credentials)
+
+    def test_new_from_json_bad_module(self):
+        creds_data = {
+            '_module': 'oauth2client.foobar',
+            '_class': 'Credentials',
+        }
+        json_data = json.dumps(creds_data)
+        with self.assertRaises(ImportError):
+            client.Credentials.new_from_json(json_data)
+
+    def test_new_from_json_bad_class(self):
+        creds_data = {
+            '_module': 'oauth2client.client',
+            '_class': 'NopeNotCredentials',
+        }
+        json_data = json.dumps(creds_data)
+        with self.assertRaises(AttributeError):
+            client.Credentials.new_from_json(json_data)
+
+    def test_from_json(self):
+        unused_data = {}
+        credentials = client.Credentials.from_json(unused_data)
+        self.assertIsInstance(credentials, client.Credentials)
+        self.assertEqual(credentials.__dict__, {})
+
+
+class TestStorage(unittest2.TestCase):
+
+    def test_locked_get_abstract(self):
+        storage = client.Storage()
+        with self.assertRaises(NotImplementedError):
+            storage.locked_get()
+
+    def test_locked_put_abstract(self):
+        storage = client.Storage()
+        credentials = object()
+        with self.assertRaises(NotImplementedError):
+            storage.locked_put(credentials)
+
+    def test_locked_delete_abstract(self):
+        storage = client.Storage()
+        with self.assertRaises(NotImplementedError):
+            storage.locked_delete()
+
+
+@contextlib.contextmanager
+def mock_module_import(module):
+    """Place a dummy objects in sys.modules to mock an import test."""
+    parts = module.split('.')
+    entries = ['.'.join(parts[:i + 1]) for i in range(len(parts))]
+    for entry in entries:
+        sys.modules[entry] = object()
+
+    try:
+        yield
+
+    finally:
+        for entry in entries:
+            del sys.modules[entry]
+
+
+class GoogleCredentialsTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.os_name = os.name
+        client.SETTINGS.env_name = None
+
+    def tearDown(self):
+        self.reset_env('SERVER_SOFTWARE')
+        self.reset_env(client.GOOGLE_APPLICATION_CREDENTIALS)
+        self.reset_env('APPDATA')
+        os.name = self.os_name
+
+    def reset_env(self, env):
+        """Set the environment variable 'env' to 'value'."""
+        os.environ.pop(env, None)
+
+    def validate_service_account_credentials(self, credentials):
+        self.assertIsInstance(
+            credentials, service_account.ServiceAccountCredentials)
+        self.assertEqual('123', credentials.client_id)
+        self.assertEqual('dummy@google.com',
+                         credentials._service_account_email)
+        self.assertEqual('ABCDEF', credentials._private_key_id)
+        self.assertEqual('', credentials._scopes)
+
+    def validate_google_credentials(self, credentials):
+        self.assertIsInstance(credentials, client.GoogleCredentials)
+        self.assertEqual(None, credentials.access_token)
+        self.assertEqual('123', credentials.client_id)
+        self.assertEqual('secret', credentials.client_secret)
+        self.assertEqual('alabalaportocala', credentials.refresh_token)
+        self.assertEqual(None, credentials.token_expiry)
+        self.assertEqual(oauth2client.GOOGLE_TOKEN_URI, credentials.token_uri)
+        self.assertEqual('Python client library', credentials.user_agent)
+
+    def get_a_google_credentials_object(self):
+        return client.GoogleCredentials(None, None, None, None,
+                                        None, None, None, None)
+
+    def test_create_scoped_required(self):
+        self.assertFalse(
+            self.get_a_google_credentials_object().create_scoped_required())
+
+    def test_create_scoped(self):
+        credentials = self.get_a_google_credentials_object()
+        self.assertEqual(credentials, credentials.create_scoped(None))
+        self.assertEqual(credentials,
+                         credentials.create_scoped(['dummy_scope']))
+
+    @mock.patch.object(client.GoogleCredentials,
+                       '_implicit_credentials_from_files',
+                       return_value=None)
+    @mock.patch.object(client.GoogleCredentials,
+                       '_implicit_credentials_from_gce')
+    @mock.patch.object(client, '_in_gae_environment',
+                       return_value=True)
+    @mock.patch.object(client, '_get_application_default_credential_GAE',
+                       return_value=object())
+    def test_get_application_default_in_gae(self, gae_adc, in_gae,
+                                            from_gce, from_files):
+        credentials = client.GoogleCredentials.get_application_default()
+        self.assertEqual(credentials, gae_adc.return_value)
+        from_files.assert_called_once_with()
+        in_gae.assert_called_once_with()
+        from_gce.assert_not_called()
+
+    @mock.patch.object(client.GoogleCredentials,
+                       '_implicit_credentials_from_gae',
+                       return_value=None)
+    @mock.patch.object(client.GoogleCredentials,
+                       '_implicit_credentials_from_files',
+                       return_value=None)
+    @mock.patch.object(client, '_in_gce_environment',
+                       return_value=True)
+    @mock.patch.object(client, '_get_application_default_credential_GCE',
+                       return_value=object())
+    def test_get_application_default_in_gce(self, gce_adc, in_gce,
+                                            from_files, from_gae):
+        credentials = client.GoogleCredentials.get_application_default()
+        self.assertEqual(credentials, gce_adc.return_value)
+        in_gce.assert_called_once_with()
+        from_gae.assert_called_once_with()
+        from_files.assert_called_once_with()
+
+    def test_environment_check_gae_production(self):
+        with mock_module_import('google.appengine'):
+            self._environment_check_gce_helper(
+                server_software='Google App Engine/XYZ')
+
+    def test_environment_check_gae_local(self):
+        with mock_module_import('google.appengine'):
+            self._environment_check_gce_helper(
+                server_software='Development/XYZ')
+
+    def test_environment_check_fastpath(self):
+        with mock_module_import('google.appengine'):
+            self._environment_check_gce_helper(
+                server_software='Development/XYZ')
+
+    def test_environment_caching(self):
+        os.environ['SERVER_SOFTWARE'] = 'Development/XYZ'
+        with mock_module_import('google.appengine'):
+            self.assertTrue(client._in_gae_environment())
+            os.environ['SERVER_SOFTWARE'] = ''
+            # Even though we no longer pass the environment check, it
+            # is cached.
+            self.assertTrue(client._in_gae_environment())
+
+    def _environment_check_gce_helper(self, status_ok=True, socket_error=False,
+                                      server_software=''):
+        response = mock.MagicMock()
+        if status_ok:
+            response.status = http_client.OK
+            response.getheader = mock.MagicMock(
+                name='getheader',
+                return_value=client._DESIRED_METADATA_FLAVOR)
+        else:
+            response.status = http_client.NOT_FOUND
+
+        connection = mock.MagicMock()
+        connection.getresponse = mock.MagicMock(name='getresponse',
+                                                return_value=response)
+        if socket_error:
+            connection.getresponse.side_effect = socket.error()
+
+        with mock.patch('oauth2client.client.os') as os_module:
+            os_module.environ = {client._SERVER_SOFTWARE: server_software}
+            with mock.patch('oauth2client.client.six') as six_module:
+                http_client_module = six_module.moves.http_client
+                http_client_module.HTTPConnection = mock.MagicMock(
+                    name='HTTPConnection', return_value=connection)
+
+                if server_software == '':
+                    self.assertFalse(client._in_gae_environment())
+                else:
+                    self.assertTrue(client._in_gae_environment())
+
+                if status_ok and not socket_error and server_software == '':
+                    self.assertTrue(client._in_gce_environment())
+                else:
+                    self.assertFalse(client._in_gce_environment())
+
+                if server_software == '':
+                    http_client_module.HTTPConnection.assert_called_once_with(
+                        client._GCE_METADATA_HOST,
+                        timeout=client.GCE_METADATA_TIMEOUT)
+                    connection.getresponse.assert_called_once_with()
+                    # Remaining calls are not "getresponse"
+                    headers = {
+                        client._METADATA_FLAVOR_HEADER: (
+                            client._DESIRED_METADATA_FLAVOR),
+                    }
+                    self.assertEqual(connection.method_calls, [
+                        mock.call.request('GET', '/',
+                                          headers=headers),
+                        mock.call.close(),
+                    ])
+                    self.assertEqual(response.method_calls, [])
+                    if status_ok and not socket_error:
+                        response.getheader.assert_called_once_with(
+                            client._METADATA_FLAVOR_HEADER)
+                else:
+                    self.assertEqual(
+                        http_client_module.HTTPConnection.mock_calls, [])
+                    self.assertEqual(connection.getresponse.mock_calls, [])
+                    # Remaining calls are not "getresponse"
+                    self.assertEqual(connection.method_calls, [])
+                    self.assertEqual(response.method_calls, [])
+                    self.assertEqual(response.getheader.mock_calls, [])
+
+    def test_environment_check_gce_production(self):
+        self._environment_check_gce_helper(status_ok=True)
+
+    def test_environment_check_gce_prod_with_working_gae_imports(self):
+        with mock_module_import('google.appengine'):
+            self._environment_check_gce_helper(status_ok=True)
+
+    def test_environment_check_gce_timeout(self):
+        self._environment_check_gce_helper(socket_error=True)
+
+    def test_environ_check_gae_module_unknown(self):
+        with mock_module_import('google.appengine'):
+            self._environment_check_gce_helper(status_ok=False)
+
+    def test_environment_check_unknown(self):
+        self._environment_check_gce_helper(status_ok=False)
+
+    def test_get_environment_variable_file(self):
+        environment_variable_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        os.environ[client.GOOGLE_APPLICATION_CREDENTIALS] = (
+            environment_variable_file)
+        self.assertEqual(environment_variable_file,
+                         client._get_environment_variable_file())
+
+    def test_get_environment_variable_file_error(self):
+        nonexistent_file = datafile('nonexistent')
+        os.environ[client.GOOGLE_APPLICATION_CREDENTIALS] = nonexistent_file
+        expected_err_msg = (
+            'File {0} \(pointed by {1} environment variable\) does not '
+            'exist!'.format(
+                nonexistent_file, client.GOOGLE_APPLICATION_CREDENTIALS))
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            client._get_environment_variable_file()
+
+    @mock.patch.dict(os.environ, {}, clear=True)
+    def test_get_environment_variable_file_without_env_var(self):
+        self.assertIsNone(client._get_environment_variable_file())
+
+    @mock.patch('os.name', new='nt')
+    @mock.patch.dict(os.environ, {'APPDATA': DATA_DIR}, clear=True)
+    def test_get_well_known_file_on_windows(self):
+        well_known_file = datafile(
+            os.path.join(client._CLOUDSDK_CONFIG_DIRECTORY,
+                         client._WELL_KNOWN_CREDENTIALS_FILE))
+        self.assertEqual(well_known_file, client._get_well_known_file())
+
+    @mock.patch('os.name', new='nt')
+    @mock.patch.dict(os.environ, {'SystemDrive': 'G:'}, clear=True)
+    def test_get_well_known_file_on_windows_without_appdata(self):
+        well_known_file = os.path.join('G:', '\\',
+                                       client._CLOUDSDK_CONFIG_DIRECTORY,
+                                       client._WELL_KNOWN_CREDENTIALS_FILE)
+        self.assertEqual(well_known_file, client._get_well_known_file())
+
+    @mock.patch.dict(os.environ,
+                     {client._CLOUDSDK_CONFIG_ENV_VAR: 'CUSTOM_DIR'},
+                     clear=True)
+    def test_get_well_known_file_with_custom_config_dir(self):
+        CUSTOM_DIR = os.environ[client._CLOUDSDK_CONFIG_ENV_VAR]
+        EXPECTED_FILE = os.path.join(CUSTOM_DIR,
+                                     client._WELL_KNOWN_CREDENTIALS_FILE)
+        well_known_file = client._get_well_known_file()
+        self.assertEqual(well_known_file, EXPECTED_FILE)
+
+    def test_get_adc_from_file_service_account(self):
+        credentials_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        credentials = client._get_application_default_credential_from_file(
+            credentials_file)
+        self.validate_service_account_credentials(credentials)
+
+    def test_save_to_well_known_file_service_account(self):
+        credential_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        credentials = client._get_application_default_credential_from_file(
+            credential_file)
+        temp_credential_file = datafile(
+            os.path.join('gcloud',
+                         'temp_well_known_file_service_account.json'))
+        client.save_to_well_known_file(credentials, temp_credential_file)
+        with open(temp_credential_file) as f:
+            d = json.load(f)
+        self.assertEqual('service_account', d['type'])
+        self.assertEqual('123', d['client_id'])
+        self.assertEqual('dummy@google.com', d['client_email'])
+        self.assertEqual('ABCDEF', d['private_key_id'])
+        os.remove(temp_credential_file)
+
+    @mock.patch('os.path.isdir', return_value=False)
+    def test_save_well_known_file_with_non_existent_config_dir(self,
+                                                               isdir_mock):
+        credential_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        credentials = client._get_application_default_credential_from_file(
+            credential_file)
+        with self.assertRaises(OSError):
+            client.save_to_well_known_file(credentials)
+        config_dir = os.path.join(os.path.expanduser('~'), '.config', 'gcloud')
+        isdir_mock.assert_called_once_with(config_dir)
+
+    def test_get_adc_from_file_authorized_user(self):
+        credentials_file = datafile(os.path.join(
+            'gcloud',
+            'application_default_credentials_authorized_user.json'))
+        credentials = client._get_application_default_credential_from_file(
+            credentials_file)
+        self.validate_google_credentials(credentials)
+
+    def test_save_to_well_known_file_authorized_user(self):
+        credentials_file = datafile(os.path.join(
+            'gcloud',
+            'application_default_credentials_authorized_user.json'))
+        credentials = client._get_application_default_credential_from_file(
+            credentials_file)
+        temp_credential_file = datafile(
+            os.path.join('gcloud',
+                         'temp_well_known_file_authorized_user.json'))
+        client.save_to_well_known_file(credentials, temp_credential_file)
+        with open(temp_credential_file) as f:
+            d = json.load(f)
+        self.assertEqual('authorized_user', d['type'])
+        self.assertEqual('123', d['client_id'])
+        self.assertEqual('secret', d['client_secret'])
+        self.assertEqual('alabalaportocala', d['refresh_token'])
+        os.remove(temp_credential_file)
+
+    def test_get_application_default_credential_from_malformed_file_1(self):
+        credentials_file = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_1.json'))
+        expected_err_msg = (
+            "'type' field should be defined \(and have one of the '{0}' or "
+            "'{1}' values\)".format(client.AUTHORIZED_USER,
+                                    client.SERVICE_ACCOUNT))
+
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            client._get_application_default_credential_from_file(
+                credentials_file)
+
+    def test_get_application_default_credential_from_malformed_file_2(self):
+        credentials_file = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_2.json'))
+        expected_err_msg = (
+            'The following field\(s\) must be defined: private_key_id')
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            client._get_application_default_credential_from_file(
+                credentials_file)
+
+    def test_get_application_default_credential_from_malformed_file_3(self):
+        credentials_file = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_3.json'))
+        with self.assertRaises(ValueError):
+            client._get_application_default_credential_from_file(
+                credentials_file)
+
+    def test_raise_exception_for_missing_fields(self):
+        missing_fields = ['first', 'second', 'third']
+        expected_err_msg = ('The following field\(s\) must be defined: ' +
+                            ', '.join(missing_fields))
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            client._raise_exception_for_missing_fields(missing_fields)
+
+    def test_raise_exception_for_reading_json(self):
+        credential_file = 'any_file'
+        extra_help = ' be good'
+        error = client.ApplicationDefaultCredentialsError('stuff happens')
+        expected_err_msg = ('An error was encountered while reading '
+                            'json file: ' + credential_file +
+                            extra_help + ': ' + str(error))
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            client._raise_exception_for_reading_json(
+                credential_file, extra_help, error)
+
+    @mock.patch('oauth2client.client._in_gce_environment')
+    @mock.patch('oauth2client.client._in_gae_environment', return_value=False)
+    @mock.patch('oauth2client.client._get_environment_variable_file')
+    @mock.patch('oauth2client.client._get_well_known_file')
+    def test_get_adc_from_env_var_service_account(self, *stubs):
+        # Set up stubs.
+        get_well_known, get_env_file, in_gae, in_gce = stubs
+        get_env_file.return_value = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+
+        credentials = client.GoogleCredentials.get_application_default()
+        self.validate_service_account_credentials(credentials)
+
+        get_env_file.assert_called_once_with()
+        get_well_known.assert_not_called()
+        in_gae.assert_not_called()
+        in_gce.assert_not_called()
+
+    def test_env_name(self):
+        self.assertEqual(None, client.SETTINGS.env_name)
+        self.test_get_adc_from_env_var_service_account()
+        self.assertEqual(client.DEFAULT_ENV_NAME, client.SETTINGS.env_name)
+
+    @mock.patch('oauth2client.client._in_gce_environment')
+    @mock.patch('oauth2client.client._in_gae_environment', return_value=False)
+    @mock.patch('oauth2client.client._get_environment_variable_file')
+    @mock.patch('oauth2client.client._get_well_known_file')
+    def test_get_adc_from_env_var_authorized_user(self, *stubs):
+        # Set up stubs.
+        get_well_known, get_env_file, in_gae, in_gce = stubs
+        get_env_file.return_value = datafile(os.path.join(
+            'gcloud',
+            'application_default_credentials_authorized_user.json'))
+
+        credentials = client.GoogleCredentials.get_application_default()
+        self.validate_google_credentials(credentials)
+
+        get_env_file.assert_called_once_with()
+        get_well_known.assert_not_called()
+        in_gae.assert_not_called()
+        in_gce.assert_not_called()
+
+    @mock.patch('oauth2client.client._in_gce_environment')
+    @mock.patch('oauth2client.client._in_gae_environment', return_value=False)
+    @mock.patch('oauth2client.client._get_environment_variable_file')
+    @mock.patch('oauth2client.client._get_well_known_file')
+    def test_get_adc_from_env_var_malformed_file(self, *stubs):
+        # Set up stubs.
+        get_well_known, get_env_file, in_gae, in_gce = stubs
+        get_env_file.return_value = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_3.json'))
+
+        expected_err = client.ApplicationDefaultCredentialsError
+        with self.assertRaises(expected_err) as exc_manager:
+            client.GoogleCredentials.get_application_default()
+
+        self.assertTrue(str(exc_manager.exception).startswith(
+            'An error was encountered while reading json file: ' +
+            get_env_file.return_value + ' (pointed to by ' +
+            client.GOOGLE_APPLICATION_CREDENTIALS + ' environment variable):'))
+
+        get_env_file.assert_called_once_with()
+        get_well_known.assert_not_called()
+        in_gae.assert_not_called()
+        in_gce.assert_not_called()
+
+    @mock.patch('oauth2client.client._in_gce_environment', return_value=False)
+    @mock.patch('oauth2client.client._in_gae_environment', return_value=False)
+    @mock.patch('oauth2client.client._get_environment_variable_file',
+                return_value=None)
+    @mock.patch('oauth2client.client._get_well_known_file',
+                return_value='BOGUS_FILE')
+    def test_get_adc_env_not_set_up(self, *stubs):
+        # Unpack stubs.
+        get_well_known, get_env_file, in_gae, in_gce = stubs
+        # Make sure the well-known file actually doesn't exist.
+        self.assertFalse(os.path.exists(get_well_known.return_value))
+
+        expected_err = client.ApplicationDefaultCredentialsError
+        with self.assertRaises(expected_err) as exc_manager:
+            client.GoogleCredentials.get_application_default()
+
+        self.assertEqual(client.ADC_HELP_MSG, str(exc_manager.exception))
+        get_env_file.assert_called_once_with()
+        get_well_known.assert_called_once_with()
+        in_gae.assert_called_once_with()
+        in_gce.assert_called_once_with()
+
+    @mock.patch('oauth2client.client._in_gce_environment', return_value=False)
+    @mock.patch('oauth2client.client._in_gae_environment', return_value=False)
+    @mock.patch('oauth2client.client._get_environment_variable_file',
+                return_value=None)
+    @mock.patch('oauth2client.client._get_well_known_file')
+    def test_get_adc_env_from_well_known(self, *stubs):
+        # Unpack stubs.
+        get_well_known, get_env_file, in_gae, in_gce = stubs
+        # Make sure the well-known file is an actual file.
+        get_well_known.return_value = __file__
+        # Make sure the well-known file actually doesn't exist.
+        self.assertTrue(os.path.exists(get_well_known.return_value))
+
+        method_name = \
+            'oauth2client.client._get_application_default_credential_from_file'
+        result_creds = object()
+        with mock.patch(method_name,
+                        return_value=result_creds) as get_from_file:
+            result = client.GoogleCredentials.get_application_default()
+            self.assertEqual(result, result_creds)
+            get_from_file.assert_called_once_with(__file__)
+
+        get_env_file.assert_called_once_with()
+        get_well_known.assert_called_once_with()
+        in_gae.assert_not_called()
+        in_gce.assert_not_called()
+
+    def test_from_stream_service_account(self):
+        credentials_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        credentials = self.get_a_google_credentials_object().from_stream(
+            credentials_file)
+        self.validate_service_account_credentials(credentials)
+
+    def test_from_stream_authorized_user(self):
+        credentials_file = datafile(os.path.join(
+            'gcloud',
+            'application_default_credentials_authorized_user.json'))
+        credentials = self.get_a_google_credentials_object().from_stream(
+            credentials_file)
+        self.validate_google_credentials(credentials)
+
+    def test_from_stream_missing_file(self):
+        credentials_filename = None
+        expected_err_msg = (r'The parameter passed to the from_stream\(\) '
+                            r'method should point to a file.')
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            self.get_a_google_credentials_object().from_stream(
+                credentials_filename)
+
+    def test_from_stream_malformed_file_1(self):
+        credentials_file = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_1.json'))
+        expected_err_msg = (
+            'An error was encountered while reading json file: ' +
+            credentials_file +
+            ' \(provided as parameter to the from_stream\(\) method\): ' +
+            "'type' field should be defined \(and have one of the '" +
+            client.AUTHORIZED_USER + "' or '" + client.SERVICE_ACCOUNT +
+            "' values\)")
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            self.get_a_google_credentials_object().from_stream(
+                credentials_file)
+
+    def test_from_stream_malformed_file_2(self):
+        credentials_file = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_2.json'))
+        expected_err_msg = (
+            'An error was encountered while reading json file: ' +
+            credentials_file +
+            ' \(provided as parameter to the from_stream\(\) method\): '
+            'The following field\(s\) must be defined: '
+            'private_key_id')
+        with self.assertRaisesRegexp(client.ApplicationDefaultCredentialsError,
+                                     expected_err_msg):
+            self.get_a_google_credentials_object().from_stream(
+                credentials_file)
+
+    def test_from_stream_malformed_file_3(self):
+        credentials_file = datafile(
+            os.path.join('gcloud',
+                         'application_default_credentials_malformed_3.json'))
+        with self.assertRaises(client.ApplicationDefaultCredentialsError):
+            self.get_a_google_credentials_object().from_stream(
+                credentials_file)
+
+    def test_to_from_json_authorized_user(self):
+        filename = 'application_default_credentials_authorized_user.json'
+        credentials_file = datafile(os.path.join('gcloud', filename))
+        creds = client.GoogleCredentials.from_stream(credentials_file)
+        json = creds.to_json()
+        creds2 = client.GoogleCredentials.from_json(json)
+
+        self.assertEqual(creds.__dict__, creds2.__dict__)
+
+    def test_to_from_json_service_account(self):
+        credentials_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        creds1 = client.GoogleCredentials.from_stream(credentials_file)
+        # Convert to and then back from json.
+        creds2 = client.GoogleCredentials.from_json(creds1.to_json())
+
+        creds1_vals = creds1.__dict__
+        creds1_vals.pop('_signer')
+        creds2_vals = creds2.__dict__
+        creds2_vals.pop('_signer')
+        self.assertEqual(creds1_vals, creds2_vals)
+
+    def test_to_from_json_service_account_scoped(self):
+        credentials_file = datafile(
+            os.path.join('gcloud', client._WELL_KNOWN_CREDENTIALS_FILE))
+        creds1 = client.GoogleCredentials.from_stream(credentials_file)
+        creds1 = creds1.create_scoped(['dummy_scope'])
+        # Convert to and then back from json.
+        creds2 = client.GoogleCredentials.from_json(creds1.to_json())
+
+        creds1_vals = creds1.__dict__
+        creds1_vals.pop('_signer')
+        creds2_vals = creds2.__dict__
+        creds2_vals.pop('_signer')
+        self.assertEqual(creds1_vals, creds2_vals)
+
+    def test_parse_expiry(self):
+        dt = datetime.datetime(2016, 1, 1)
+        parsed_expiry = client._parse_expiry(dt)
+        self.assertEqual('2016-01-01T00:00:00Z', parsed_expiry)
+
+    def test_bad_expiry(self):
+        dt = object()
+        parsed_expiry = client._parse_expiry(dt)
+        self.assertEqual(None, parsed_expiry)
+
+
+class DummyDeleteStorage(client.Storage):
+    delete_called = False
+
+    def locked_delete(self):
+        self.delete_called = True
+
+
+def _token_revoke_test_helper(testcase, status, revoke_raise,
+                              valid_bool_value, token_attr):
+    current_store = getattr(testcase.credentials, 'store', None)
+
+    dummy_store = DummyDeleteStorage()
+    testcase.credentials.set_store(dummy_store)
+
+    actual_do_revoke = testcase.credentials._do_revoke
+    testcase.token_from_revoke = None
+
+    def do_revoke_stub(http_request, token):
+        testcase.token_from_revoke = token
+        return actual_do_revoke(http_request, token)
+    testcase.credentials._do_revoke = do_revoke_stub
+
+    http = HttpMock(headers={'status': status})
+    if revoke_raise:
+        testcase.assertRaises(client.TokenRevokeError,
+                              testcase.credentials.revoke, http)
+    else:
+        testcase.credentials.revoke(http)
+
+    testcase.assertEqual(getattr(testcase.credentials, token_attr),
+                         testcase.token_from_revoke)
+    testcase.assertEqual(valid_bool_value, testcase.credentials.invalid)
+    testcase.assertEqual(valid_bool_value, dummy_store.delete_called)
+
+    testcase.credentials.set_store(current_store)
+
+
+class BasicCredentialsTests(unittest2.TestCase):
+
+    def setUp(self):
+        access_token = 'foo'
+        client_id = 'some_client_id'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = datetime.datetime.utcnow()
+        user_agent = 'refresh_checker/1.0'
+        self.credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, oauth2client.GOOGLE_TOKEN_URI,
+            user_agent, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+            scopes='foo', token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI)
+
+        # Provoke a failure if @util.positional is not respected.
+        self.old_positional_enforcement = (
+            util.positional_parameters_enforcement)
+        util.positional_parameters_enforcement = (
+            util.POSITIONAL_EXCEPTION)
+
+    def tearDown(self):
+        util.positional_parameters_enforcement = (
+            self.old_positional_enforcement)
+
+    def test_token_refresh_success(self):
+        for status_code in client.REFRESH_STATUS_CODES:
+            token_response = {'access_token': '1/3w', 'expires_in': 3600}
+            http = HttpMockSequence([
+                ({'status': status_code}, b''),
+                ({'status': '200'}, json.dumps(token_response).encode(
+                    'utf-8')),
+                ({'status': '200'}, 'echo_request_headers'),
+            ])
+            http = self.credentials.authorize(http)
+            resp, content = http.request('http://example.com')
+            self.assertEqual(b'Bearer 1/3w', content[b'Authorization'])
+            self.assertFalse(self.credentials.access_token_expired)
+            self.assertEqual(token_response, self.credentials.token_response)
+
+    def test_recursive_authorize(self):
+        """Tests that OAuth2Credentials doesn't intro. new method constraints.
+
+        Formerly, OAuth2Credentials.authorize monkeypatched the request method
+        of its httplib2.Http argument with a wrapper annotated with
+        @util.positional(1). Since the original method has no such annotation,
+        that meant that the wrapper was violating the contract of the original
+        method by adding a new requirement to it. And in fact the wrapper
+        itself doesn't even respect that requirement. So before the removal of
+        the annotation, this test would fail.
+        """
+        token_response = {'access_token': '1/3w', 'expires_in': 3600}
+        encoded_response = json.dumps(token_response).encode('utf-8')
+        http = HttpMockSequence([
+            ({'status': '200'}, encoded_response),
+        ])
+        http = self.credentials.authorize(http)
+        http = self.credentials.authorize(http)
+        http.request('http://example.com')
+
+    def test_token_refresh_failure(self):
+        for status_code in client.REFRESH_STATUS_CODES:
+            http = HttpMockSequence([
+                ({'status': status_code}, b''),
+                ({'status': http_client.BAD_REQUEST},
+                 b'{"error":"access_denied"}'),
+            ])
+            http = self.credentials.authorize(http)
+            with self.assertRaises(
+                    client.HttpAccessTokenRefreshError) as exc_manager:
+                http.request('http://example.com')
+            self.assertEqual(http_client.BAD_REQUEST,
+                             exc_manager.exception.status)
+            self.assertTrue(self.credentials.access_token_expired)
+            self.assertEqual(None, self.credentials.token_response)
+
+    def test_token_revoke_success(self):
+        _token_revoke_test_helper(
+            self, '200', revoke_raise=False,
+            valid_bool_value=True, token_attr='refresh_token')
+
+    def test_token_revoke_failure(self):
+        _token_revoke_test_helper(
+            self, '400', revoke_raise=True,
+            valid_bool_value=False, token_attr='refresh_token')
+
+    def test_token_revoke_fallback(self):
+        original_credentials = self.credentials.to_json()
+        self.credentials.refresh_token = None
+        _token_revoke_test_helper(
+            self, '200', revoke_raise=False,
+            valid_bool_value=True, token_attr='access_token')
+        self.credentials = self.credentials.from_json(original_credentials)
+
+    def test_non_401_error_response(self):
+        http = HttpMockSequence([
+            ({'status': '400'}, b''),
+        ])
+        http = self.credentials.authorize(http)
+        resp, content = http.request('http://example.com')
+        self.assertEqual(http_client.BAD_REQUEST, resp.status)
+        self.assertEqual(None, self.credentials.token_response)
+
+    def test_to_from_json(self):
+        json = self.credentials.to_json()
+        instance = client.OAuth2Credentials.from_json(json)
+        self.assertEqual(client.OAuth2Credentials, type(instance))
+        instance.token_expiry = None
+        self.credentials.token_expiry = None
+
+        self.assertEqual(instance.__dict__, self.credentials.__dict__)
+
+    def test_from_json_token_expiry(self):
+        data = json.loads(self.credentials.to_json())
+        data['token_expiry'] = None
+        instance = client.OAuth2Credentials.from_json(json.dumps(data))
+        self.assertIsInstance(instance, client.OAuth2Credentials)
+
+    def test_from_json_bad_token_expiry(self):
+        data = json.loads(self.credentials.to_json())
+        data['token_expiry'] = 'foobar'
+        instance = client.OAuth2Credentials.from_json(json.dumps(data))
+        self.assertIsInstance(instance, client.OAuth2Credentials)
+
+    def test_unicode_header_checks(self):
+        access_token = u'foo'
+        client_id = u'some_client_id'
+        client_secret = u'cOuDdkfjxxnv+'
+        refresh_token = u'1/0/a.df219fjls0'
+        token_expiry = str(datetime.datetime.utcnow())
+        token_uri = str(oauth2client.GOOGLE_TOKEN_URI)
+        revoke_uri = str(oauth2client.GOOGLE_REVOKE_URI)
+        user_agent = u'refresh_checker/1.0'
+        credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret, refresh_token,
+            token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
+
+        # First, test that we correctly encode basic objects, making sure
+        # to include a bytes object. Note that oauth2client will normalize
+        # everything to bytes, no matter what python version we're in.
+        http = credentials.authorize(HttpMock())
+        headers = {u'foo': 3, b'bar': True, 'baz': b'abc'}
+        cleaned_headers = {b'foo': b'3', b'bar': b'True', b'baz': b'abc'}
+        http.request(u'http://example.com', method=u'GET', headers=headers)
+        for k, v in cleaned_headers.items():
+            self.assertTrue(k in http.headers)
+            self.assertEqual(v, http.headers[k])
+
+        # Next, test that we do fail on unicode.
+        unicode_str = six.unichr(40960) + 'abcd'
+        with self.assertRaises(client.NonAsciiHeaderError):
+            http.request(u'http://example.com', method=u'GET',
+                         headers={u'foo': unicode_str})
+
+    def test_no_unicode_in_request_params(self):
+        access_token = u'foo'
+        client_id = u'some_client_id'
+        client_secret = u'cOuDdkfjxxnv+'
+        refresh_token = u'1/0/a.df219fjls0'
+        token_expiry = str(datetime.datetime.utcnow())
+        token_uri = str(oauth2client.GOOGLE_TOKEN_URI)
+        revoke_uri = str(oauth2client.GOOGLE_REVOKE_URI)
+        user_agent = u'refresh_checker/1.0'
+        credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret, refresh_token,
+            token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
+
+        http = HttpMock()
+        http = credentials.authorize(http)
+        http.request(u'http://example.com', method=u'GET',
+                     headers={u'foo': u'bar'})
+        for k, v in six.iteritems(http.headers):
+            self.assertIsInstance(k, six.binary_type)
+            self.assertIsInstance(v, six.binary_type)
+
+        # Test again with unicode strings that can't simply be converted
+        # to ASCII.
+        with self.assertRaises(client.NonAsciiHeaderError):
+            http.request(
+                u'http://example.com', method=u'GET',
+                headers={u'foo': u'\N{COMET}'})
+
+        self.credentials.token_response = 'foobar'
+        instance = client.OAuth2Credentials.from_json(
+            self.credentials.to_json())
+        self.assertEqual('foobar', instance.token_response)
+
+    def test__expires_in_no_expiry(self):
+        credentials = client.OAuth2Credentials(None, None, None, None,
+                                               None, None, None)
+        self.assertIsNone(credentials.token_expiry)
+        self.assertIsNone(credentials._expires_in())
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test__expires_in_expired(self, utcnow):
+        credentials = client.OAuth2Credentials(None, None, None, None,
+                                               None, None, None)
+        credentials.token_expiry = datetime.datetime.utcnow()
+        now = credentials.token_expiry + datetime.timedelta(seconds=1)
+        self.assertLess(credentials.token_expiry, now)
+        utcnow.return_value = now
+        self.assertEqual(credentials._expires_in(), 0)
+        utcnow.assert_called_once_with()
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test__expires_in_not_expired(self, utcnow):
+        credentials = client.OAuth2Credentials(None, None, None, None,
+                                               None, None, None)
+        credentials.token_expiry = datetime.datetime.utcnow()
+        seconds = 1234
+        now = credentials.token_expiry - datetime.timedelta(seconds=seconds)
+        self.assertLess(now, credentials.token_expiry)
+        utcnow.return_value = now
+        self.assertEqual(credentials._expires_in(), seconds)
+        utcnow.assert_called_once_with()
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_get_access_token(self, utcnow):
+        # Configure the patch.
+        seconds = 11
+        NOW = datetime.datetime(1992, 12, 31, second=seconds)
+        utcnow.return_value = NOW
+
+        lifetime = 2  # number of seconds in which the token expires
+        EXPIRY_TIME = datetime.datetime(1992, 12, 31,
+                                        second=seconds + lifetime)
+
+        token1 = u'first_token'
+        token_response_first = {
+            'access_token': token1,
+            'expires_in': lifetime,
+        }
+        token2 = u'second_token'
+        token_response_second = {
+            'access_token': token2,
+            'expires_in': lifetime,
+        }
+        http = HttpMockSequence([
+            ({'status': '200'}, json.dumps(token_response_first).encode(
+                'utf-8')),
+            ({'status': '200'}, json.dumps(token_response_second).encode(
+                'utf-8')),
+        ])
+
+        # Use the current credentials but unset the expiry and
+        # the access token.
+        credentials = copy.deepcopy(self.credentials)
+        credentials.access_token = None
+        credentials.token_expiry = None
+
+        # Get Access Token, First attempt.
+        self.assertEqual(credentials.access_token, None)
+        self.assertFalse(credentials.access_token_expired)
+        self.assertEqual(credentials.token_expiry, None)
+        token = credentials.get_access_token(http=http)
+        self.assertEqual(credentials.token_expiry, EXPIRY_TIME)
+        self.assertEqual(token1, token.access_token)
+        self.assertEqual(lifetime, token.expires_in)
+        self.assertEqual(token_response_first, credentials.token_response)
+        # Two utcnow calls are expected:
+        # - get_access_token() -> _do_refresh_request (setting expires in)
+        # - get_access_token() -> _expires_in()
+        expected_utcnow_calls = [mock.call()] * 2
+        self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
+
+        # Get Access Token, Second Attempt (not expired)
+        self.assertEqual(credentials.access_token, token1)
+        self.assertFalse(credentials.access_token_expired)
+        token = credentials.get_access_token(http=http)
+        # Make sure no refresh occurred since the token was not expired.
+        self.assertEqual(token1, token.access_token)
+        self.assertEqual(lifetime, token.expires_in)
+        self.assertEqual(token_response_first, credentials.token_response)
+        # Three more utcnow calls are expected:
+        # - access_token_expired
+        # - get_access_token() -> access_token_expired
+        # - get_access_token -> _expires_in
+        expected_utcnow_calls = [mock.call()] * (2 + 3)
+        self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
+
+        # Get Access Token, Third Attempt (force expiration)
+        self.assertEqual(credentials.access_token, token1)
+        credentials.token_expiry = NOW  # Manually force expiry.
+        self.assertTrue(credentials.access_token_expired)
+        token = credentials.get_access_token(http=http)
+        # Make sure refresh occurred since the token was not expired.
+        self.assertEqual(token2, token.access_token)
+        self.assertEqual(lifetime, token.expires_in)
+        self.assertFalse(credentials.access_token_expired)
+        self.assertEqual(token_response_second,
+                         credentials.token_response)
+        # Five more utcnow calls are expected:
+        # - access_token_expired
+        # - get_access_token -> access_token_expired
+        # - get_access_token -> _do_refresh_request
+        # - get_access_token -> _expires_in
+        # - access_token_expired
+        expected_utcnow_calls = [mock.call()] * (2 + 3 + 5)
+        self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
+
+    @mock.patch.object(client.OAuth2Credentials, 'refresh')
+    @mock.patch.object(client.OAuth2Credentials, '_expires_in',
+                       return_value=1835)
+    def test_get_access_token_without_http(self, expires_in, refresh_mock):
+        credentials = client.OAuth2Credentials(None, None, None, None,
+                                               None, None, None)
+        # Make sure access_token_expired returns True
+        credentials.invalid = True
+        # Specify a token so we can use it in the response.
+        credentials.access_token = 'ya29-s3kr3t'
+
+        with mock.patch('httplib2.Http',
+                        return_value=object) as http_kls:
+            token_info = credentials.get_access_token()
+            expires_in.assert_called_once_with()
+            refresh_mock.assert_called_once_with(http_kls.return_value)
+
+        self.assertIsInstance(token_info, client.AccessTokenInfo)
+        self.assertEqual(token_info.access_token,
+                         credentials.access_token)
+        self.assertEqual(token_info.expires_in,
+                         expires_in.return_value)
+
+    @mock.patch.object(client.OAuth2Credentials, 'refresh')
+    @mock.patch.object(client.OAuth2Credentials, '_expires_in',
+                       return_value=1835)
+    def test_get_access_token_with_http(self, expires_in, refresh_mock):
+        credentials = client.OAuth2Credentials(None, None, None, None,
+                                               None, None, None)
+        # Make sure access_token_expired returns True
+        credentials.invalid = True
+        # Specify a token so we can use it in the response.
+        credentials.access_token = 'ya29-s3kr3t'
+
+        http_obj = object()
+        token_info = credentials.get_access_token(http_obj)
+        self.assertIsInstance(token_info, client.AccessTokenInfo)
+        self.assertEqual(token_info.access_token,
+                         credentials.access_token)
+        self.assertEqual(token_info.expires_in,
+                         expires_in.return_value)
+
+        expires_in.assert_called_once_with()
+        refresh_mock.assert_called_once_with(http_obj)
+
+    @mock.patch.object(client.OAuth2Credentials,
+                       '_generate_refresh_request_headers',
+                       return_value=object())
+    @mock.patch.object(client.OAuth2Credentials,
+                       '_generate_refresh_request_body',
+                       return_value=object())
+    @mock.patch('oauth2client.client.logger')
+    def _do_refresh_request_test_helper(self, response, content,
+                                        error_msg, logger, gen_body,
+                                        gen_headers, store=None):
+        credentials = client.OAuth2Credentials(None, None, None, None,
+                                               None, None, None)
+        credentials.store = store
+        http_request = mock.Mock()
+        http_request.return_value = response, content
+
+        with self.assertRaises(
+                client.HttpAccessTokenRefreshError) as exc_manager:
+            credentials._do_refresh_request(http_request)
+
+        self.assertEqual(exc_manager.exception.args, (error_msg,))
+        self.assertEqual(exc_manager.exception.status, response.status)
+        http_request.assert_called_once_with(None, body=gen_body.return_value,
+                                             headers=gen_headers.return_value,
+                                             method='POST')
+
+        call1 = mock.call('Refreshing access_token')
+        failure_template = 'Failed to retrieve access token: %s'
+        call2 = mock.call(failure_template, content)
+        self.assertEqual(logger.info.mock_calls, [call1, call2])
+        if store is not None:
+            store.locked_put.assert_called_once_with(credentials)
+
+    def test__do_refresh_request_non_json_failure(self):
+        response = httplib2.Response({
+            'status': int(http_client.BAD_REQUEST),
+        })
+        content = u'Bad request'
+        error_msg = 'Invalid response {0}.'.format(int(response.status))
+        self._do_refresh_request_test_helper(response, content, error_msg)
+
+    def test__do_refresh_request_basic_failure(self):
+        response = httplib2.Response({
+            'status': int(http_client.INTERNAL_SERVER_ERROR),
+        })
+        content = u'{}'
+        error_msg = 'Invalid response {0}.'.format(int(response.status))
+        self._do_refresh_request_test_helper(response, content, error_msg)
+
+    def test__do_refresh_request_failure_w_json_error(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_GATEWAY,
+        })
+        error_msg = 'Hi I am an error not a bearer'
+        content = json.dumps({'error': error_msg})
+        self._do_refresh_request_test_helper(response, content, error_msg)
+
+    def test__do_refresh_request_failure_w_json_error_and_store(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_GATEWAY,
+        })
+        error_msg = 'Where are we going wearer?'
+        content = json.dumps({'error': error_msg})
+        store = mock.MagicMock()
+        self._do_refresh_request_test_helper(response, content, error_msg,
+                                             store=store)
+
+    def test__do_refresh_request_failure_w_json_error_and_desc(self):
+        response = httplib2.Response({
+            'status': http_client.SERVICE_UNAVAILABLE,
+        })
+        base_error = 'Ruckus'
+        error_desc = 'Can you describe the ruckus'
+        content = json.dumps({
+            'error': base_error,
+            'error_description': error_desc,
+        })
+        error_msg = '{0}: {1}'.format(base_error, error_desc)
+        self._do_refresh_request_test_helper(response, content, error_msg)
+
+    @mock.patch('oauth2client.client.logger')
+    def _do_revoke_test_helper(self, response, content,
+                               error_msg, logger, store=None):
+        credentials = client.OAuth2Credentials(
+            None, None, None, None, None, None, None,
+            revoke_uri=oauth2client.GOOGLE_REVOKE_URI)
+        credentials.store = store
+        http_request = mock.Mock()
+        http_request.return_value = response, content
+        token = u's3kr3tz'
+
+        if response.status == http_client.OK:
+            self.assertFalse(credentials.invalid)
+            self.assertIsNone(credentials._do_revoke(http_request, token))
+            self.assertTrue(credentials.invalid)
+            if store is not None:
+                store.delete.assert_called_once_with()
+        else:
+            self.assertFalse(credentials.invalid)
+            with self.assertRaises(client.TokenRevokeError) as exc_manager:
+                credentials._do_revoke(http_request, token)
+            # Make sure invalid was not flipped on.
+            self.assertFalse(credentials.invalid)
+            self.assertEqual(exc_manager.exception.args, (error_msg,))
+            if store is not None:
+                store.delete.assert_not_called()
+
+        revoke_uri = oauth2client.GOOGLE_REVOKE_URI + '?token=' + token
+        http_request.assert_called_once_with(revoke_uri)
+
+        logger.info.assert_called_once_with('Revoking token')
+
+    def test__do_revoke_success(self):
+        response = httplib2.Response({
+            'status': http_client.OK,
+        })
+        self._do_revoke_test_helper(response, b'', None)
+
+    def test__do_revoke_success_with_store(self):
+        response = httplib2.Response({
+            'status': http_client.OK,
+        })
+        store = mock.MagicMock()
+        self._do_revoke_test_helper(response, b'', None, store=store)
+
+    def test__do_revoke_non_json_failure(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_REQUEST,
+        })
+        content = u'Bad request'
+        error_msg = 'Invalid response {0}.'.format(response.status)
+        self._do_revoke_test_helper(response, content, error_msg)
+
+    def test__do_revoke_basic_failure(self):
+        response = httplib2.Response({
+            'status': http_client.INTERNAL_SERVER_ERROR,
+        })
+        content = u'{}'
+        error_msg = 'Invalid response {0}.'.format(response.status)
+        self._do_revoke_test_helper(response, content, error_msg)
+
+    def test__do_revoke_failure_w_json_error(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_GATEWAY,
+        })
+        error_msg = 'Hi I am an error not a bearer'
+        content = json.dumps({'error': error_msg})
+        self._do_revoke_test_helper(response, content, error_msg)
+
+    def test__do_revoke_failure_w_json_error_and_store(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_GATEWAY,
+        })
+        error_msg = 'Where are we going wearer?'
+        content = json.dumps({'error': error_msg})
+        store = mock.MagicMock()
+        self._do_revoke_test_helper(response, content, error_msg,
+                                    store=store)
+
+    @mock.patch('oauth2client.client.logger')
+    def _do_retrieve_scopes_test_helper(self, response, content,
+                                        error_msg, logger, scopes=None):
+        credentials = client.OAuth2Credentials(
+            None, None, None, None, None, None, None,
+            token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI)
+        http_request = mock.Mock()
+        http_request.return_value = response, content
+        token = u's3kr3tz'
+
+        if response.status == http_client.OK:
+            self.assertEqual(credentials.scopes, set())
+            self.assertIsNone(
+                credentials._do_retrieve_scopes(http_request, token))
+            self.assertEqual(credentials.scopes, scopes)
+        else:
+            self.assertEqual(credentials.scopes, set())
+            with self.assertRaises(client.Error) as exc_manager:
+                credentials._do_retrieve_scopes(http_request, token)
+            # Make sure scopes were not changed.
+            self.assertEqual(credentials.scopes, set())
+            self.assertEqual(exc_manager.exception.args, (error_msg,))
+
+        token_uri = client._update_query_params(
+            oauth2client.GOOGLE_TOKEN_INFO_URI,
+            {'fields': 'scope', 'access_token': token})
+        self.assertEqual(len(http_request.mock_calls), 1)
+        scopes_call = http_request.mock_calls[0]
+        call_args = scopes_call[1]
+        self.assertEqual(len(call_args), 1)
+        called_uri = call_args[0]
+        assertUrisEqual(self, token_uri, called_uri)
+        logger.info.assert_called_once_with('Refreshing scopes')
+
+    def test__do_retrieve_scopes_success_bad_json(self):
+        response = httplib2.Response({
+            'status': http_client.OK,
+        })
+        invalid_json = b'{'
+        with self.assertRaises(ValueError):
+            self._do_retrieve_scopes_test_helper(response, invalid_json, None)
+
+    def test__do_retrieve_scopes_success(self):
+        response = httplib2.Response({
+            'status': http_client.OK,
+        })
+        content = b'{"scope": "foo bar"}'
+        self._do_retrieve_scopes_test_helper(response, content, None,
+                                             scopes=set(['foo', 'bar']))
+
+    def test__do_retrieve_scopes_non_json_failure(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_REQUEST,
+        })
+        content = u'Bad request'
+        error_msg = 'Invalid response {0}.'.format(response.status)
+        self._do_retrieve_scopes_test_helper(response, content, error_msg)
+
+    def test__do_retrieve_scopes_basic_failure(self):
+        response = httplib2.Response({
+            'status': http_client.INTERNAL_SERVER_ERROR,
+        })
+        content = u'{}'
+        error_msg = 'Invalid response {0}.'.format(response.status)
+        self._do_retrieve_scopes_test_helper(response, content, error_msg)
+
+    def test__do_retrieve_scopes_failure_w_json_error(self):
+        response = httplib2.Response({
+            'status': http_client.BAD_GATEWAY,
+        })
+        error_msg = 'Error desc I sit at a desk'
+        content = json.dumps({'error_description': error_msg})
+        self._do_retrieve_scopes_test_helper(response, content, error_msg)
+
+    def test_has_scopes(self):
+        self.assertTrue(self.credentials.has_scopes('foo'))
+        self.assertTrue(self.credentials.has_scopes(['foo']))
+        self.assertFalse(self.credentials.has_scopes('bar'))
+        self.assertFalse(self.credentials.has_scopes(['bar']))
+
+        self.credentials.scopes = set(['foo', 'bar'])
+        self.assertTrue(self.credentials.has_scopes('foo'))
+        self.assertTrue(self.credentials.has_scopes('bar'))
+        self.assertFalse(self.credentials.has_scopes('baz'))
+        self.assertTrue(self.credentials.has_scopes(['foo', 'bar']))
+        self.assertFalse(self.credentials.has_scopes(['foo', 'baz']))
+
+        self.credentials.scopes = set([])
+        self.assertFalse(self.credentials.has_scopes('foo'))
+
+    def test_retrieve_scopes(self):
+        info_response_first = {'scope': 'foo bar'}
+        info_response_second = {'error_description': 'abcdef'}
+        http = HttpMockSequence([
+            ({'status': '200'}, json.dumps(info_response_first).encode(
+                'utf-8')),
+            ({'status': '400'}, json.dumps(info_response_second).encode(
+                'utf-8')),
+            ({'status': '500'}, b''),
+        ])
+
+        self.credentials.retrieve_scopes(http)
+        self.assertEqual(set(['foo', 'bar']), self.credentials.scopes)
+
+        with self.assertRaises(client.Error):
+            self.credentials.retrieve_scopes(http)
+
+        with self.assertRaises(client.Error):
+            self.credentials.retrieve_scopes(http)
+
+    def test_refresh_updates_id_token(self):
+        for status_code in client.REFRESH_STATUS_CODES:
+            body = {'foo': 'bar'}
+            body_json = json.dumps(body).encode('ascii')
+            payload = base64.urlsafe_b64encode(body_json).strip(b'=')
+            jwt = b'stuff.' + payload + b'.signature'
+
+            token_response = (b'{'
+                              b'  "access_token":"1/3w",'
+                              b'  "expires_in":3600,'
+                              b'  "id_token": "' + jwt + b'"'
+                              b'}')
+            http = HttpMockSequence([
+                ({'status': status_code}, b''),
+                ({'status': '200'}, token_response),
+                ({'status': '200'}, 'echo_request_headers'),
+            ])
+            http = self.credentials.authorize(http)
+            resp, content = http.request('http://example.com')
+            self.assertEqual(self.credentials.id_token, body)
+
+
+class AccessTokenCredentialsTests(unittest2.TestCase):
+
+    def setUp(self):
+        access_token = 'foo'
+        user_agent = 'refresh_checker/1.0'
+        self.credentials = client.AccessTokenCredentials(
+            access_token, user_agent,
+            revoke_uri=oauth2client.GOOGLE_REVOKE_URI)
+
+    def test_token_refresh_success(self):
+        for status_code in client.REFRESH_STATUS_CODES:
+            http = HttpMockSequence([
+                ({'status': status_code}, b''),
+            ])
+            http = self.credentials.authorize(http)
+            with self.assertRaises(client.AccessTokenCredentialsError):
+                resp, content = http.request('http://example.com')
+
+    def test_token_revoke_success(self):
+        _token_revoke_test_helper(
+            self, '200', revoke_raise=False,
+            valid_bool_value=True, token_attr='access_token')
+
+    def test_token_revoke_failure(self):
+        _token_revoke_test_helper(
+            self, '400', revoke_raise=True,
+            valid_bool_value=False, token_attr='access_token')
+
+    def test_non_401_error_response(self):
+        http = HttpMockSequence([
+            ({'status': '400'}, b''),
+        ])
+        http = self.credentials.authorize(http)
+        resp, content = http.request('http://example.com')
+        self.assertEqual(http_client.BAD_REQUEST, resp.status)
+
+    def test_auth_header_sent(self):
+        http = HttpMockSequence([
+            ({'status': '200'}, 'echo_request_headers'),
+        ])
+        http = self.credentials.authorize(http)
+        resp, content = http.request('http://example.com')
+        self.assertEqual(b'Bearer foo', content[b'Authorization'])
+
+
+class TestAssertionCredentials(unittest2.TestCase):
+    assertion_text = 'This is the assertion'
+    assertion_type = 'http://www.google.com/assertionType'
+
+    class AssertionCredentialsTestImpl(client.AssertionCredentials):
+
+        def _generate_assertion(self):
+            return TestAssertionCredentials.assertion_text
+
+    def setUp(self):
+        user_agent = 'fun/2.0'
+        self.credentials = self.AssertionCredentialsTestImpl(
+            self.assertion_type, user_agent=user_agent)
+
+    def test__generate_assertion_abstract(self):
+        credentials = client.AssertionCredentials(None)
+        with self.assertRaises(NotImplementedError):
+            credentials._generate_assertion()
+
+    def test_assertion_body(self):
+        body = urllib.parse.parse_qs(
+            self.credentials._generate_refresh_request_body())
+        self.assertEqual(self.assertion_text, body['assertion'][0])
+        self.assertEqual('urn:ietf:params:oauth:grant-type:jwt-bearer',
+                         body['grant_type'][0])
+
+    def test_assertion_refresh(self):
+        http = HttpMockSequence([
+            ({'status': '200'}, b'{"access_token":"1/3w"}'),
+            ({'status': '200'}, 'echo_request_headers'),
+        ])
+        http = self.credentials.authorize(http)
+        resp, content = http.request('http://example.com')
+        self.assertEqual(b'Bearer 1/3w', content[b'Authorization'])
+
+    def test_token_revoke_success(self):
+        _token_revoke_test_helper(
+            self, '200', revoke_raise=False,
+            valid_bool_value=True, token_attr='access_token')
+
+    def test_token_revoke_failure(self):
+        _token_revoke_test_helper(
+            self, '400', revoke_raise=True,
+            valid_bool_value=False, token_attr='access_token')
+
+    def test_sign_blob_abstract(self):
+        credentials = client.AssertionCredentials(None)
+        with self.assertRaises(NotImplementedError):
+            credentials.sign_blob(b'blob')
+
+
+class UpdateQueryParamsTest(unittest2.TestCase):
+    def test_update_query_params_no_params(self):
+        uri = 'http://www.google.com'
+        updated = client._update_query_params(uri, {'a': 'b'})
+        self.assertEqual(updated, uri + '?a=b')
+
+    def test_update_query_params_existing_params(self):
+        uri = 'http://www.google.com?x=y'
+        updated = client._update_query_params(uri, {'a': 'b', 'c': 'd&'})
+        hardcoded_update = uri + '&a=b&c=d%26'
+        assertUrisEqual(self, updated, hardcoded_update)
+
+
+class ExtractIdTokenTest(unittest2.TestCase):
+    """Tests client._extract_id_token()."""
+
+    def test_extract_success(self):
+        body = {'foo': 'bar'}
+        body_json = json.dumps(body).encode('ascii')
+        payload = base64.urlsafe_b64encode(body_json).strip(b'=')
+        jwt = b'stuff.' + payload + b'.signature'
+
+        extracted = client._extract_id_token(jwt)
+        self.assertEqual(extracted, body)
+
+    def test_extract_failure(self):
+        body = {'foo': 'bar'}
+        body_json = json.dumps(body).encode('ascii')
+        payload = base64.urlsafe_b64encode(body_json).strip(b'=')
+        jwt = b'stuff.' + payload
+        with self.assertRaises(client.VerifyJwtTokenError):
+            client._extract_id_token(jwt)
+
+
+class OAuth2WebServerFlowTest(unittest2.TestCase):
+
+    def setUp(self):
+        self.flow = client.OAuth2WebServerFlow(
+            client_id='client_id+1',
+            client_secret='secret+1',
+            scope='foo',
+            redirect_uri=client.OOB_CALLBACK_URN,
+            user_agent='unittest-sample/1.0',
+            revoke_uri='dummy_revoke_uri',
+        )
+
+    def test_construct_authorize_url(self):
+        authorize_url = self.flow.step1_get_authorize_url(state='state+1')
+
+        parsed = urllib.parse.urlparse(authorize_url)
+        q = urllib.parse.parse_qs(parsed[4])
+        self.assertEqual('client_id+1', q['client_id'][0])
+        self.assertEqual('code', q['response_type'][0])
+        self.assertEqual('foo', q['scope'][0])
+        self.assertEqual(client.OOB_CALLBACK_URN, q['redirect_uri'][0])
+        self.assertEqual('offline', q['access_type'][0])
+        self.assertEqual('state+1', q['state'][0])
+
+    def test_override_flow_via_kwargs(self):
+        """Passing kwargs to override defaults."""
+        flow = client.OAuth2WebServerFlow(
+            client_id='client_id+1',
+            client_secret='secret+1',
+            scope='foo',
+            redirect_uri=client.OOB_CALLBACK_URN,
+            user_agent='unittest-sample/1.0',
+            access_type='online',
+            response_type='token'
+        )
+        authorize_url = flow.step1_get_authorize_url()
+
+        parsed = urllib.parse.urlparse(authorize_url)
+        q = urllib.parse.parse_qs(parsed[4])
+        self.assertEqual('client_id+1', q['client_id'][0])
+        self.assertEqual('token', q['response_type'][0])
+        self.assertEqual('foo', q['scope'][0])
+        self.assertEqual(client.OOB_CALLBACK_URN, q['redirect_uri'][0])
+        self.assertEqual('online', q['access_type'][0])
+
+    def test__oauth2_web_server_flow_params(self):
+        params = client._oauth2_web_server_flow_params({})
+        self.assertEqual(params['access_type'], 'offline')
+        self.assertEqual(params['response_type'], 'code')
+
+        params = client._oauth2_web_server_flow_params({
+            'approval_prompt': 'force'})
+        self.assertEqual(params['prompt'], 'consent')
+        self.assertNotIn('approval_prompt', params)
+
+        params = client._oauth2_web_server_flow_params({
+            'approval_prompt': 'other'})
+        self.assertEqual(params['approval_prompt'], 'other')
+
+    @mock.patch('oauth2client.client.logger')
+    def test_step1_get_authorize_url_redirect_override(self, logger):
+        flow = client.OAuth2WebServerFlow('client_id+1', scope='foo',
+                                          redirect_uri=client.OOB_CALLBACK_URN)
+        alt_redirect = 'foo:bar'
+        self.assertEqual(flow.redirect_uri, client.OOB_CALLBACK_URN)
+        result = flow.step1_get_authorize_url(redirect_uri=alt_redirect)
+        # Make sure the redirect value was updated.
+        self.assertEqual(flow.redirect_uri, alt_redirect)
+        query_params = {
+            'client_id': flow.client_id,
+            'redirect_uri': alt_redirect,
+            'scope': flow.scope,
+            'access_type': 'offline',
+            'response_type': 'code',
+        }
+        expected = client._update_query_params(flow.auth_uri, query_params)
+        assertUrisEqual(self, expected, result)
+        # Check stubs.
+        self.assertEqual(logger.warning.call_count, 1)
+
+    def test_step1_get_authorize_url_without_redirect(self):
+        flow = client.OAuth2WebServerFlow('client_id+1', scope='foo',
+                                          redirect_uri=None)
+        with self.assertRaises(ValueError):
+            flow.step1_get_authorize_url(redirect_uri=None)
+
+    def test_step1_get_authorize_url_without_login_hint(self):
+        login_hint = 'There are wascally wabbits nearby'
+        flow = client.OAuth2WebServerFlow('client_id+1', scope='foo',
+                                          redirect_uri=client.OOB_CALLBACK_URN,
+                                          login_hint=login_hint)
+        result = flow.step1_get_authorize_url()
+        query_params = {
+            'client_id': flow.client_id,
+            'login_hint': login_hint,
+            'redirect_uri': client.OOB_CALLBACK_URN,
+            'scope': flow.scope,
+            'access_type': 'offline',
+            'response_type': 'code',
+        }
+        expected = client._update_query_params(flow.auth_uri, query_params)
+        assertUrisEqual(self, expected, result)
+
+    def test_step1_get_device_and_user_codes_wo_device_uri(self):
+        flow = client.OAuth2WebServerFlow('CID', scope='foo', device_uri=None)
+        with self.assertRaises(ValueError):
+            flow.step1_get_device_and_user_codes()
+
+    def _step1_get_device_and_user_codes_helper(
+            self, extra_headers=None, user_agent=None, default_http=False,
+            content=None):
+        flow = client.OAuth2WebServerFlow('CID', scope='foo',
+                                          user_agent=user_agent)
+        device_code = 'bfc06756-062e-430f-9f0f-460ca44724e5'
+        user_code = '5faf2780-fc83-11e5-9bc2-00c2c63e5792'
+        ver_url = 'http://foo.bar'
+        if content is None:
+            content = json.dumps({
+                'device_code': device_code,
+                'user_code': user_code,
+                'verification_url': ver_url,
+            })
+        http = HttpMockSequence([
+            ({'status': http_client.OK}, content),
+        ])
+        if default_http:
+            with mock.patch('httplib2.Http', return_value=http):
+                result = flow.step1_get_device_and_user_codes()
+        else:
+            result = flow.step1_get_device_and_user_codes(http=http)
+
+        expected = client.DeviceFlowInfo(
+            device_code, user_code, None, ver_url, None)
+        self.assertEqual(result, expected)
+        self.assertEqual(len(http.requests), 1)
+        self.assertEqual(
+            http.requests[0]['uri'], oauth2client.GOOGLE_DEVICE_URI)
+        body = http.requests[0]['body']
+        self.assertEqual(urllib.parse.parse_qs(body),
+                         {'client_id': [flow.client_id],
+                          'scope': [flow.scope]})
+        headers = {'content-type': 'application/x-www-form-urlencoded'}
+        if extra_headers is not None:
+            headers.update(extra_headers)
+        self.assertEqual(http.requests[0]['headers'], headers)
+
+    def test_step1_get_device_and_user_codes(self):
+        self._step1_get_device_and_user_codes_helper()
+
+    def test_step1_get_device_and_user_codes_w_user_agent(self):
+        user_agent = 'spiderman'
+        extra_headers = {'user-agent': user_agent}
+        self._step1_get_device_and_user_codes_helper(
+            user_agent=user_agent, extra_headers=extra_headers)
+
+    def test_step1_get_device_and_user_codes_w_default_http(self):
+        self._step1_get_device_and_user_codes_helper(default_http=True)
+
+    def test_step1_get_device_and_user_codes_bad_payload(self):
+        non_json_content = b'{'
+        with self.assertRaises(client.OAuth2DeviceCodeError):
+            self._step1_get_device_and_user_codes_helper(
+                content=non_json_content)
+
+    def _step1_get_device_and_user_codes_fail_helper(self, status,
+                                                     content, error_msg):
+        flow = client.OAuth2WebServerFlow('CID', scope='foo')
+        http = HttpMockSequence([
+            ({'status': status}, content),
+        ])
+        with self.assertRaises(client.OAuth2DeviceCodeError) as exc_manager:
+            flow.step1_get_device_and_user_codes(http=http)
+
+        self.assertEqual(exc_manager.exception.args, (error_msg,))
+
+    def test_step1_get_device_and_user_codes_non_json_failure(self):
+        status = int(http_client.BAD_REQUEST)
+        content = 'Nope not JSON.'
+        error_msg = 'Invalid response {0}.'.format(status)
+        self._step1_get_device_and_user_codes_fail_helper(status, content,
+                                                          error_msg)
+
+    def test_step1_get_device_and_user_codes_basic_failure(self):
+        status = int(http_client.INTERNAL_SERVER_ERROR)
+        content = b'{}'
+        error_msg = 'Invalid response {0}.'.format(status)
+        self._step1_get_device_and_user_codes_fail_helper(status, content,
+                                                          error_msg)
+
+    def test_step1_get_device_and_user_codes_failure_w_json_error(self):
+        status = int(http_client.BAD_GATEWAY)
+        base_error = 'ZOMG user codes failure.'
+        content = json.dumps({'error': base_error})
+        error_msg = 'Invalid response {0}. Error: {1}'.format(status,
+                                                              base_error)
+        self._step1_get_device_and_user_codes_fail_helper(status, content,
+                                                          error_msg)
+
+    def test_step2_exchange_no_input(self):
+        flow = client.OAuth2WebServerFlow('client_id+1', scope='foo')
+        with self.assertRaises(ValueError):
+            flow.step2_exchange()
+
+    def test_step2_exchange_code_and_device_flow(self):
+        flow = client.OAuth2WebServerFlow('client_id+1', scope='foo')
+        with self.assertRaises(ValueError):
+            flow.step2_exchange(code='code', device_flow_info='dfi')
+
+    def test_scope_is_required(self):
+        with self.assertRaises(TypeError):
+            client.OAuth2WebServerFlow('client_id+1')
+
+    def test_exchange_failure(self):
+        http = HttpMockSequence([
+            ({'status': '400'}, b'{"error":"invalid_request"}'),
+        ])
+
+        with self.assertRaises(client.FlowExchangeError):
+            self.flow.step2_exchange(code='some random code', http=http)
+
+    def test_urlencoded_exchange_failure(self):
+        http = HttpMockSequence([
+            ({'status': '400'}, b'error=invalid_request'),
+        ])
+
+        with self.assertRaisesRegexp(client.FlowExchangeError,
+                                     'invalid_request'):
+            self.flow.step2_exchange(code='some random code', http=http)
+
+    def test_exchange_failure_with_json_error(self):
+        # Some providers have 'error' attribute as a JSON object
+        # in place of regular string.
+        # This test makes sure no strange object-to-string coversion
+        # exceptions are being raised instead of FlowExchangeError.
+        payload = (b'{'
+                   b'  "error": {'
+                   b'    "message": "Error validating verification code.",'
+                   b'    "type": "OAuthException"'
+                   b'  }'
+                   b'}')
+        http = HttpMockSequence([({'status': '400'}, payload)])
+
+        with self.assertRaises(client.FlowExchangeError):
+            self.flow.step2_exchange(code='some random code', http=http)
+
+    def _exchange_success_test_helper(self, code=None, device_flow_info=None):
+        payload = (b'{'
+                   b'  "access_token":"SlAV32hkKG",'
+                   b'  "expires_in":3600,'
+                   b'  "refresh_token":"8xLOxBtZp8"'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+        credentials = self.flow.step2_exchange(
+            code=code, device_flow_info=device_flow_info, http=http)
+        self.assertEqual('SlAV32hkKG', credentials.access_token)
+        self.assertNotEqual(None, credentials.token_expiry)
+        self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
+        self.assertEqual('dummy_revoke_uri', credentials.revoke_uri)
+        self.assertEqual(set(['foo']), credentials.scopes)
+
+    def test_exchange_success(self):
+        self._exchange_success_test_helper(code='some random code')
+
+    def test_exchange_success_with_device_flow_info(self):
+        device_flow_info = client.DeviceFlowInfo(
+            'some random code', None, None, None, None)
+        self._exchange_success_test_helper(device_flow_info=device_flow_info)
+
+    def test_exchange_success_binary_code(self):
+        binary_code = b'some random code'
+        access_token = 'SlAV32hkKG'
+        expires_in = '3600'
+        refresh_token = '8xLOxBtZp8'
+        revoke_uri = 'dummy_revoke_uri'
+
+        payload = ('{'
+                   '  "access_token":"' + access_token + '",'
+                   '  "expires_in":' + expires_in + ','
+                   '  "refresh_token":"' + refresh_token + '"'
+                   '}')
+        http = HttpMockSequence(
+            [({'status': '200'}, _helpers._to_bytes(payload))])
+        credentials = self.flow.step2_exchange(code=binary_code, http=http)
+        self.assertEqual(access_token, credentials.access_token)
+        self.assertIsNotNone(credentials.token_expiry)
+        self.assertEqual(refresh_token, credentials.refresh_token)
+        self.assertEqual(revoke_uri, credentials.revoke_uri)
+        self.assertEqual(set(['foo']), credentials.scopes)
+
+    def test_exchange_dictlike(self):
+        class FakeDict(object):
+            def __init__(self, d):
+                self.d = d
+
+            def __getitem__(self, name):
+                return self.d[name]
+
+            def __contains__(self, name):
+                return name in self.d
+
+        code = 'some random code'
+        not_a_dict = FakeDict({'code': code})
+        payload = (b'{'
+                   b'  "access_token":"SlAV32hkKG",'
+                   b'  "expires_in":3600,'
+                   b'  "refresh_token":"8xLOxBtZp8"'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+
+        credentials = self.flow.step2_exchange(code=not_a_dict, http=http)
+        self.assertEqual('SlAV32hkKG', credentials.access_token)
+        self.assertNotEqual(None, credentials.token_expiry)
+        self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
+        self.assertEqual('dummy_revoke_uri', credentials.revoke_uri)
+        self.assertEqual(set(['foo']), credentials.scopes)
+        request_code = urllib.parse.parse_qs(
+            http.requests[0]['body'])['code'][0]
+        self.assertEqual(code, request_code)
+
+    def test_exchange_using_authorization_header(self):
+        auth_header = 'Basic Y2xpZW50X2lkKzE6c2Vjexc_managerV0KzE=',
+        flow = client.OAuth2WebServerFlow(
+            client_id='client_id+1',
+            authorization_header=auth_header,
+            scope='foo',
+            redirect_uri=client.OOB_CALLBACK_URN,
+            user_agent='unittest-sample/1.0',
+            revoke_uri='dummy_revoke_uri',
+        )
+        http = HttpMockSequence([
+            ({'status': '200'}, b'access_token=SlAV32hkKG'),
+        ])
+
+        credentials = flow.step2_exchange(code='some random code', http=http)
+        self.assertEqual('SlAV32hkKG', credentials.access_token)
+
+        test_request = http.requests[0]
+        # Did we pass the Authorization header?
+        self.assertEqual(test_request['headers']['Authorization'], auth_header)
+        # Did we omit client_secret from POST body?
+        self.assertTrue('client_secret' not in test_request['body'])
+
+    def test_urlencoded_exchange_success(self):
+        http = HttpMockSequence([
+            ({'status': '200'}, b'access_token=SlAV32hkKG&expires_in=3600'),
+        ])
+
+        credentials = self.flow.step2_exchange(code='some random code',
+                                               http=http)
+        self.assertEqual('SlAV32hkKG', credentials.access_token)
+        self.assertNotEqual(None, credentials.token_expiry)
+
+    def test_urlencoded_expires_param(self):
+        http = HttpMockSequence([
+            # Note the 'expires=3600' where you'd normally
+            # have if named 'expires_in'
+            ({'status': '200'}, b'access_token=SlAV32hkKG&expires=3600'),
+        ])
+
+        credentials = self.flow.step2_exchange(code='some random code',
+                                               http=http)
+        self.assertNotEqual(None, credentials.token_expiry)
+
+    def test_exchange_no_expires_in(self):
+        payload = (b'{'
+                   b'  "access_token":"SlAV32hkKG",'
+                   b'  "refresh_token":"8xLOxBtZp8"'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+
+        credentials = self.flow.step2_exchange(code='some random code',
+                                               http=http)
+        self.assertEqual(None, credentials.token_expiry)
+
+    def test_urlencoded_exchange_no_expires_in(self):
+        http = HttpMockSequence([
+            # This might be redundant but just to make sure
+            # urlencoded access_token gets parsed correctly
+            ({'status': '200'}, b'access_token=SlAV32hkKG'),
+        ])
+
+        credentials = self.flow.step2_exchange(code='some random code',
+                                               http=http)
+        self.assertEqual(None, credentials.token_expiry)
+
+    def test_exchange_fails_if_no_code(self):
+        payload = (b'{'
+                   b'  "access_token":"SlAV32hkKG",'
+                   b'  "refresh_token":"8xLOxBtZp8"'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+
+        code = {'error': 'thou shall not pass'}
+        with self.assertRaisesRegexp(
+                client.FlowExchangeError, 'shall not pass'):
+            self.flow.step2_exchange(code=code, http=http)
+
+    def test_exchange_id_token_fail(self):
+        payload = (b'{'
+                   b'  "access_token":"SlAV32hkKG",'
+                   b'  "refresh_token":"8xLOxBtZp8",'
+                   b'  "id_token": "stuff.payload"'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+
+        with self.assertRaises(client.VerifyJwtTokenError):
+            self.flow.step2_exchange(code='some random code', http=http)
+
+    def test_exchange_id_token(self):
+        body = {'foo': 'bar'}
+        body_json = json.dumps(body).encode('ascii')
+        payload = base64.urlsafe_b64encode(body_json).strip(b'=')
+        jwt = (base64.urlsafe_b64encode(b'stuff') + b'.' + payload + b'.' +
+               base64.urlsafe_b64encode(b'signature'))
+
+        payload = (b'{'
+                   b'  "access_token":"SlAV32hkKG",'
+                   b'  "refresh_token":"8xLOxBtZp8",'
+                   b'  "id_token": "' + jwt + b'"'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+        credentials = self.flow.step2_exchange(code='some random code',
+                                               http=http)
+        self.assertEqual(credentials.id_token, body)
+
+
+class FlowFromCachedClientsecrets(unittest2.TestCase):
+
+    def test_flow_from_clientsecrets_cached(self):
+        cache_mock = CacheMock()
+        load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
+
+        flow = client.flow_from_clientsecrets(
+            'some_secrets', '', redirect_uri='oob', cache=cache_mock)
+        self.assertEqual('foo_client_secret', flow.client_secret)
+
+    @mock.patch('oauth2client.clientsecrets.loadfile')
+    def _flow_from_clientsecrets_success_helper(self, loadfile_mock,
+                                                device_uri=None,
+                                                revoke_uri=None):
+        client_type = clientsecrets.TYPE_WEB
+        client_info = {
+            'auth_uri': 'auth_uri',
+            'token_uri': 'token_uri',
+            'client_id': 'client_id',
+            'client_secret': 'client_secret',
+        }
+        if revoke_uri is not None:
+            client_info['revoke_uri'] = revoke_uri
+        loadfile_mock.return_value = client_type, client_info
+        filename = object()
+        scope = ['baz']
+        cache = object()
+
+        if device_uri is not None:
+            result = client.flow_from_clientsecrets(
+                filename, scope, cache=cache, device_uri=device_uri)
+            self.assertEqual(result.device_uri, device_uri)
+        else:
+            result = client.flow_from_clientsecrets(
+                filename, scope, cache=cache)
+
+        self.assertIsInstance(result, client.OAuth2WebServerFlow)
+        loadfile_mock.assert_called_once_with(filename, cache=cache)
+
+    def test_flow_from_clientsecrets_success(self):
+        self._flow_from_clientsecrets_success_helper()
+
+    def test_flow_from_clientsecrets_success_w_device_uri(self):
+        device_uri = 'http://device.uri'
+        self._flow_from_clientsecrets_success_helper(device_uri=device_uri)
+
+    def test_flow_from_clientsecrets_success_w_revoke_uri(self):
+        revoke_uri = 'http://revoke.uri'
+        self._flow_from_clientsecrets_success_helper(revoke_uri=revoke_uri)
+
+    @mock.patch('oauth2client.clientsecrets.loadfile',
+                side_effect=clientsecrets.InvalidClientSecretsError)
+    def test_flow_from_clientsecrets_invalid(self, loadfile_mock):
+        filename = object()
+        cache = object()
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            client.flow_from_clientsecrets(
+                filename, None, cache=cache, message=None)
+        loadfile_mock.assert_called_once_with(filename, cache=cache)
+
+    @mock.patch('oauth2client.clientsecrets.loadfile',
+                side_effect=clientsecrets.InvalidClientSecretsError)
+    @mock.patch('sys.exit')
+    def test_flow_from_clientsecrets_invalid_w_msg(self, sys_exit,
+                                                   loadfile_mock):
+        filename = object()
+        cache = object()
+        message = 'hi mom'
+
+        client.flow_from_clientsecrets(
+            filename, None, cache=cache, message=message)
+        sys_exit.assert_called_once_with(message)
+        loadfile_mock.assert_called_once_with(filename, cache=cache)
+
+    @mock.patch('oauth2client.clientsecrets.loadfile',
+                side_effect=clientsecrets.InvalidClientSecretsError('foobar'))
+    @mock.patch('sys.exit')
+    def test_flow_from_clientsecrets_invalid_w_msg_and_text(self, sys_exit,
+                                                            loadfile_mock):
+        filename = object()
+        cache = object()
+        message = 'hi mom'
+        expected = ('The client secrets were invalid: '
+                    '\n{0}\n{1}'.format('foobar', 'hi mom'))
+
+        client.flow_from_clientsecrets(
+            filename, None, cache=cache, message=message)
+        sys_exit.assert_called_once_with(expected)
+        loadfile_mock.assert_called_once_with(filename, cache=cache)
+
+    @mock.patch('oauth2client.clientsecrets.loadfile')
+    def test_flow_from_clientsecrets_unknown_flow(self, loadfile_mock):
+        client_type = 'UNKNOWN'
+        loadfile_mock.return_value = client_type, None
+        filename = object()
+        cache = object()
+
+        err_msg = ('This OAuth 2.0 flow is unsupported: '
+                   '{0!r}'.format(client_type))
+        with self.assertRaisesRegexp(client.UnknownClientSecretsFlowError,
+                                     err_msg):
+            client.flow_from_clientsecrets(filename, None, cache=cache)
+
+        loadfile_mock.assert_called_once_with(filename, cache=cache)
+
+
+class CredentialsFromCodeTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.client_id = 'client_id_abc'
+        self.client_secret = 'secret_use_code'
+        self.scope = 'foo'
+        self.code = '12345abcde'
+        self.redirect_uri = 'postmessage'
+
+    def test_exchange_code_for_token(self):
+        token = 'asdfghjkl'
+        payload = json.dumps({'access_token': token, 'expires_in': 3600})
+        http = HttpMockSequence([
+            ({'status': '200'}, payload.encode('utf-8')),
+        ])
+        credentials = client.credentials_from_code(
+            self.client_id, self.client_secret, self.scope,
+            self.code, http=http, redirect_uri=self.redirect_uri)
+        self.assertEqual(credentials.access_token, token)
+        self.assertNotEqual(None, credentials.token_expiry)
+        self.assertEqual(set(['foo']), credentials.scopes)
+
+    def test_exchange_code_for_token_fail(self):
+        http = HttpMockSequence([
+            ({'status': '400'}, b'{"error":"invalid_request"}'),
+        ])
+
+        with self.assertRaises(client.FlowExchangeError):
+            client.credentials_from_code(
+                self.client_id, self.client_secret, self.scope,
+                self.code, http=http, redirect_uri=self.redirect_uri)
+
+    def test_exchange_code_and_file_for_token(self):
+        payload = (b'{'
+                   b'  "access_token":"asdfghjkl",'
+                   b'  "expires_in":3600'
+                   b'}')
+        http = HttpMockSequence([({'status': '200'}, payload)])
+        credentials = client.credentials_from_clientsecrets_and_code(
+            datafile('client_secrets.json'), self.scope,
+            self.code, http=http)
+        self.assertEqual(credentials.access_token, 'asdfghjkl')
+        self.assertNotEqual(None, credentials.token_expiry)
+        self.assertEqual(set(['foo']), credentials.scopes)
+
+    def test_exchange_code_and_cached_file_for_token(self):
+        http = HttpMockSequence([
+            ({'status': '200'}, b'{ "access_token":"asdfghjkl"}'),
+        ])
+        cache_mock = CacheMock()
+        load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
+
+        credentials = client.credentials_from_clientsecrets_and_code(
+            'some_secrets', self.scope,
+            self.code, http=http, cache=cache_mock)
+        self.assertEqual(credentials.access_token, 'asdfghjkl')
+        self.assertEqual(set(['foo']), credentials.scopes)
+
+    def test_exchange_code_and_file_for_token_fail(self):
+        http = HttpMockSequence([
+            ({'status': '400'}, b'{"error":"invalid_request"}'),
+        ])
+
+        with self.assertRaises(client.FlowExchangeError):
+            client.credentials_from_clientsecrets_and_code(
+                datafile('client_secrets.json'), self.scope,
+                self.code, http=http)
+
+
+class Test__save_private_file(unittest2.TestCase):
+
+    def _save_helper(self, filename):
+        contents = []
+        contents_str = '[]'
+        client._save_private_file(filename, contents)
+        with open(filename, 'r') as f:
+            stored_contents = f.read()
+        self.assertEqual(stored_contents, contents_str)
+
+        stat_mode = os.stat(filename).st_mode
+        # Octal 777, only last 3 positions matter for permissions mask.
+        stat_mode &= 0o777
+        self.assertEqual(stat_mode, 0o600)
+
+    def test_new(self):
+        filename = tempfile.mktemp()
+        self.assertFalse(os.path.exists(filename))
+        self._save_helper(filename)
+
+    def test_existing(self):
+        filename = tempfile.mktemp()
+        with open(filename, 'w') as f:
+            f.write('a bunch of nonsense longer than []')
+        self.assertTrue(os.path.exists(filename))
+        self._save_helper(filename)
+
+
+class Test__get_application_default_credential_GAE(unittest2.TestCase):
+
+    @mock.patch.dict('sys.modules', {
+        'oauth2client.contrib.appengine': mock.Mock()})
+    def test_it(self):
+        gae_mod = sys.modules['oauth2client.contrib.appengine']
+        gae_mod.AppAssertionCredentials = creds_kls = mock.Mock()
+        creds_kls.return_value = object()
+        credentials = client._get_application_default_credential_GAE()
+        self.assertEqual(credentials, creds_kls.return_value)
+        creds_kls.assert_called_once_with([])
+
+
+class Test__get_application_default_credential_GCE(unittest2.TestCase):
+
+    @mock.patch.dict('sys.modules', {
+        'oauth2client.contrib.gce': mock.Mock()})
+    def test_it(self):
+        gce_mod = sys.modules['oauth2client.contrib.gce']
+        gce_mod.AppAssertionCredentials = creds_kls = mock.Mock()
+        creds_kls.return_value = object()
+        credentials = client._get_application_default_credential_GCE()
+        self.assertEqual(credentials, creds_kls.return_value)
+        creds_kls.assert_called_once_with()
+
+
+class Test__require_crypto_or_die(unittest2.TestCase):
+
+    @mock.patch.object(client, 'HAS_CRYPTO', new=True)
+    def test_with_crypto(self):
+        self.assertIsNone(client._require_crypto_or_die())
+
+    @mock.patch.object(client, 'HAS_CRYPTO', new=False)
+    def test_without_crypto(self):
+        with self.assertRaises(client.CryptoUnavailableError):
+            client._require_crypto_or_die()
+
+
+class TestDeviceFlowInfo(unittest2.TestCase):
+
+    DEVICE_CODE = 'e80ff179-fd65-416c-9dbf-56a23e5d23e4'
+    USER_CODE = '4bbd8b82-fc73-11e5-adf3-00c2c63e5792'
+    VER_URL = 'http://foo.bar'
+
+    def test_FromResponse(self):
+        response = {
+            'device_code': self.DEVICE_CODE,
+            'user_code': self.USER_CODE,
+            'verification_url': self.VER_URL,
+        }
+        result = client.DeviceFlowInfo.FromResponse(response)
+        expected_result = client.DeviceFlowInfo(
+            self.DEVICE_CODE, self.USER_CODE, None, self.VER_URL, None)
+        self.assertEqual(result, expected_result)
+
+    def test_FromResponse_fallback_to_uri(self):
+        response = {
+            'device_code': self.DEVICE_CODE,
+            'user_code': self.USER_CODE,
+            'verification_uri': self.VER_URL,
+        }
+        result = client.DeviceFlowInfo.FromResponse(response)
+        expected_result = client.DeviceFlowInfo(
+            self.DEVICE_CODE, self.USER_CODE, None, self.VER_URL, None)
+        self.assertEqual(result, expected_result)
+
+    def test_FromResponse_missing_url(self):
+        response = {
+            'device_code': self.DEVICE_CODE,
+            'user_code': self.USER_CODE,
+        }
+        with self.assertRaises(client.OAuth2DeviceCodeError):
+            client.DeviceFlowInfo.FromResponse(response)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_FromResponse_with_expires_in(self, utcnow):
+        expires_in = 23
+        response = {
+            'device_code': self.DEVICE_CODE,
+            'user_code': self.USER_CODE,
+            'verification_url': self.VER_URL,
+            'expires_in': expires_in,
+        }
+        now = datetime.datetime(1999, 1, 1, 12, 30, 27)
+        expire = datetime.datetime(1999, 1, 1, 12, 30, 27 + expires_in)
+        utcnow.return_value = now
+
+        result = client.DeviceFlowInfo.FromResponse(response)
+        expected_result = client.DeviceFlowInfo(
+            self.DEVICE_CODE, self.USER_CODE, None, self.VER_URL, expire)
+        self.assertEqual(result, expected_result)
diff --git a/tests/test_clientsecrets.py b/tests/test_clientsecrets.py
new file mode 100644
index 0000000..42eb8c7
--- /dev/null
+++ b/tests/test_clientsecrets.py
@@ -0,0 +1,280 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for oauth2client.clientsecrets."""
+
+import errno
+from io import StringIO
+import os
+import tempfile
+
+import unittest2
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import clientsecrets
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+VALID_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
+INVALID_FILE = os.path.join(DATA_DIR, 'unfilled_client_secrets.json')
+NONEXISTENT_FILE = os.path.join(
+    os.path.dirname(__file__), 'afilethatisntthere.json')
+
+
+class Test__validate_clientsecrets(unittest2.TestCase):
+
+    def test_with_none(self):
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets(None)
+
+    def test_with_other_than_one_key(self):
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets({})
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets({'one': 'val', 'two': 'val'})
+
+    def test_with_non_dictionary(self):
+        non_dict = [None]
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets(non_dict)
+
+    def test_invalid_client_type(self):
+        fake_type = 'fake_type'
+        self.assertNotEqual(fake_type, clientsecrets.TYPE_WEB)
+        self.assertNotEqual(fake_type, clientsecrets.TYPE_INSTALLED)
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets({fake_type: None})
+
+    def test_missing_required_type_web(self):
+        required = clientsecrets.VALID_CLIENT[
+            clientsecrets.TYPE_WEB]['required']
+        # We will certainly have less than all 5 keys.
+        self.assertEqual(len(required), 5)
+
+        clientsecrets_dict = {
+            clientsecrets.TYPE_WEB: {'not_required': None},
+        }
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets(clientsecrets_dict)
+
+    def test_string_not_configured_type_web(self):
+        string_props = clientsecrets.VALID_CLIENT[
+            clientsecrets.TYPE_WEB]['string']
+
+        self.assertTrue('client_id' in string_props)
+        clientsecrets_dict = {
+            clientsecrets.TYPE_WEB: {
+                'client_id': '[[template]]',
+                'client_secret': 'seekrit',
+                'redirect_uris': None,
+                'auth_uri': None,
+                'token_uri': None,
+            },
+        }
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets(clientsecrets_dict)
+
+    def test_missing_required_type_installed(self):
+        required = clientsecrets.VALID_CLIENT[
+            clientsecrets.TYPE_INSTALLED]['required']
+        # We will certainly have less than all 5 keys.
+        self.assertEqual(len(required), 5)
+
+        clientsecrets_dict = {
+            clientsecrets.TYPE_INSTALLED: {'not_required': None},
+        }
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets(clientsecrets_dict)
+
+    def test_string_not_configured_type_installed(self):
+        string_props = clientsecrets.VALID_CLIENT[
+            clientsecrets.TYPE_INSTALLED]['string']
+
+        self.assertTrue('client_id' in string_props)
+        clientsecrets_dict = {
+            clientsecrets.TYPE_INSTALLED: {
+                'client_id': '[[template]]',
+                'client_secret': 'seekrit',
+                'redirect_uris': None,
+                'auth_uri': None,
+                'token_uri': None,
+            },
+        }
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._validate_clientsecrets(clientsecrets_dict)
+
+    def test_success_type_web(self):
+        client_info = {
+            'client_id': 'eye-dee',
+            'client_secret': 'seekrit',
+            'redirect_uris': None,
+            'auth_uri': None,
+            'token_uri': None,
+        }
+        clientsecrets_dict = {
+            clientsecrets.TYPE_WEB: client_info,
+        }
+        result = clientsecrets._validate_clientsecrets(clientsecrets_dict)
+        self.assertEqual(result, (clientsecrets.TYPE_WEB, client_info))
+
+    def test_success_type_installed(self):
+        client_info = {
+            'client_id': 'eye-dee',
+            'client_secret': 'seekrit',
+            'redirect_uris': None,
+            'auth_uri': None,
+            'token_uri': None,
+        }
+        clientsecrets_dict = {
+            clientsecrets.TYPE_INSTALLED: client_info,
+        }
+        result = clientsecrets._validate_clientsecrets(clientsecrets_dict)
+        self.assertEqual(result, (clientsecrets.TYPE_INSTALLED, client_info))
+
+
+class Test__loadfile(unittest2.TestCase):
+
+    def test_success(self):
+        client_type, client_info = clientsecrets._loadfile(VALID_FILE)
+        expected_client_info = {
+            'client_id': 'foo_client_id',
+            'client_secret': 'foo_client_secret',
+            'redirect_uris': [],
+            'auth_uri': oauth2client.GOOGLE_AUTH_URI,
+            'token_uri': oauth2client.GOOGLE_TOKEN_URI,
+            'revoke_uri': oauth2client.GOOGLE_REVOKE_URI,
+        }
+        self.assertEqual(client_type, clientsecrets.TYPE_WEB)
+        self.assertEqual(client_info, expected_client_info)
+
+    def test_non_existent(self):
+        path = os.path.join(DATA_DIR, 'fake.json')
+        self.assertFalse(os.path.exists(path))
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets._loadfile(path)
+
+    def test_bad_json(self):
+        filename = tempfile.mktemp()
+        with open(filename, 'wb') as file_obj:
+            file_obj.write(b'[')
+        with self.assertRaises(ValueError):
+            clientsecrets._loadfile(filename)
+
+
+class OAuth2CredentialsTests(unittest2.TestCase):
+
+    def test_validate_error(self):
+        payload = (
+            b'{'
+            b'  "web": {'
+            b'    "client_id": "[[CLIENT ID REQUIRED]]",'
+            b'    "client_secret": "[[CLIENT SECRET REQUIRED]]",'
+            b'    "redirect_uris": ["http://localhost:8080/oauth2callback"],'
+            b'    "auth_uri": "",'
+            b'    "token_uri": ""'
+            b'  }'
+            b'}')
+        ERRORS = [
+            ('{}', 'Invalid'),
+            ('{"foo": {}}', 'Unknown'),
+            ('{"web": {}}', 'Missing'),
+            ('{"web": {"client_id": "dkkd"}}', 'Missing'),
+            (payload, 'Property'),
+        ]
+        for src, match in ERRORS:
+            # Ensure that it is unicode
+            src = _helpers._from_bytes(src)
+            # Test load(s)
+            with self.assertRaises(
+                    clientsecrets.InvalidClientSecretsError) as exc_manager:
+                clientsecrets.loads(src)
+
+            self.assertTrue(str(exc_manager.exception).startswith(match))
+
+            # Test loads(fp)
+            with self.assertRaises(
+                    clientsecrets.InvalidClientSecretsError) as exc_manager:
+                fp = StringIO(src)
+                clientsecrets.load(fp)
+
+            self.assertTrue(str(exc_manager.exception).startswith(match))
+
+    def test_load_by_filename_missing_file(self):
+        with self.assertRaises(
+                clientsecrets.InvalidClientSecretsError) as exc_manager:
+            clientsecrets._loadfile(NONEXISTENT_FILE)
+
+        self.assertEquals(exc_manager.exception.args[1], NONEXISTENT_FILE)
+        self.assertEquals(exc_manager.exception.args[3], errno.ENOENT)
+
+
+class CachedClientsecretsTests(unittest2.TestCase):
+
+    class CacheMock(object):
+        def __init__(self):
+            self.cache = {}
+            self.last_get_ns = None
+            self.last_set_ns = None
+
+        def get(self, key, namespace=''):
+            # ignoring namespace for easier testing
+            self.last_get_ns = namespace
+            return self.cache.get(key, None)
+
+        def set(self, key, value, namespace=''):
+            # ignoring namespace for easier testing
+            self.last_set_ns = namespace
+            self.cache[key] = value
+
+    def setUp(self):
+        self.cache_mock = self.CacheMock()
+
+    def test_cache_miss(self):
+        client_type, client_info = clientsecrets.loadfile(
+            VALID_FILE, cache=self.cache_mock)
+        self.assertEqual('web', client_type)
+        self.assertEqual('foo_client_secret', client_info['client_secret'])
+
+        cached = self.cache_mock.cache[VALID_FILE]
+        self.assertEqual({client_type: client_info}, cached)
+
+        # make sure we're using non-empty namespace
+        ns = self.cache_mock.last_set_ns
+        self.assertTrue(bool(ns))
+        # make sure they're equal
+        self.assertEqual(ns, self.cache_mock.last_get_ns)
+
+    def test_cache_hit(self):
+        self.cache_mock.cache[NONEXISTENT_FILE] = {'web': 'secret info'}
+
+        client_type, client_info = clientsecrets.loadfile(
+            NONEXISTENT_FILE, cache=self.cache_mock)
+        self.assertEqual('web', client_type)
+        self.assertEqual('secret info', client_info)
+        # make sure we didn't do any set() RPCs
+        self.assertEqual(None, self.cache_mock.last_set_ns)
+
+    def test_validation(self):
+        with self.assertRaises(clientsecrets.InvalidClientSecretsError):
+            clientsecrets.loadfile(INVALID_FILE, cache=self.cache_mock)
+
+    def test_without_cache(self):
+        # this also ensures loadfile() is backward compatible
+        client_type, client_info = clientsecrets.loadfile(VALID_FILE)
+        self.assertEqual('web', client_type)
+        self.assertEqual('foo_client_secret', client_info['client_secret'])
diff --git a/tests/test_crypt.py b/tests/test_crypt.py
new file mode 100644
index 0000000..b7534bd
--- /dev/null
+++ b/tests/test_crypt.py
@@ -0,0 +1,313 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+import base64
+import os
+
+import mock
+import unittest2
+
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import crypt
+from oauth2client import service_account
+
+
+def data_filename(filename):
+    return os.path.join(os.path.dirname(__file__), 'data', filename)
+
+
+def datafile(filename):
+    with open(data_filename(filename), 'rb') as file_obj:
+        return file_obj.read()
+
+
+class Test__bad_pkcs12_key_as_pem(unittest2.TestCase):
+
+    def test_fails(self):
+        with self.assertRaises(NotImplementedError):
+            crypt._bad_pkcs12_key_as_pem()
+
+
+class Test_pkcs12_key_as_pem(unittest2.TestCase):
+
+    def _make_svc_account_creds(self, private_key_file='privatekey.p12'):
+        filename = data_filename(private_key_file)
+        credentials = (
+            service_account.ServiceAccountCredentials.from_p12_keyfile(
+                'some_account@example.com', filename,
+                scopes='read+write'))
+        credentials._kwargs['sub'] = 'joe@example.org'
+        return credentials
+
+    def _succeeds_helper(self, password=None):
+        self.assertEqual(True, client.HAS_OPENSSL)
+
+        credentials = self._make_svc_account_creds()
+        if password is None:
+            password = credentials._private_key_password
+        pem_contents = crypt.pkcs12_key_as_pem(
+            credentials._private_key_pkcs12, password)
+        pkcs12_key_as_pem = datafile('pem_from_pkcs12.pem')
+        pkcs12_key_as_pem = _helpers._parse_pem_key(pkcs12_key_as_pem)
+        alternate_pem = datafile('pem_from_pkcs12_alternate.pem')
+        self.assertTrue(pem_contents in [pkcs12_key_as_pem, alternate_pem])
+
+    def test_succeeds(self):
+        self._succeeds_helper()
+
+    def test_succeeds_with_unicode_password(self):
+        password = u'notasecret'
+        self._succeeds_helper(password)
+
+
+class Test__verify_signature(unittest2.TestCase):
+
+    def test_success_single_cert(self):
+        cert_value = 'cert-value'
+        certs = [cert_value]
+        message = object()
+        signature = object()
+
+        verifier = mock.MagicMock()
+        verifier.verify = mock.MagicMock(name='verify', return_value=True)
+        with mock.patch('oauth2client.crypt.Verifier') as Verifier:
+            Verifier.from_string = mock.MagicMock(name='from_string',
+                                                  return_value=verifier)
+            result = crypt._verify_signature(message, signature, certs)
+            self.assertEqual(result, None)
+
+            # Make sure our mocks were called as expected.
+            Verifier.from_string.assert_called_once_with(cert_value,
+                                                         is_x509_cert=True)
+            verifier.verify.assert_called_once_with(message, signature)
+
+    def test_success_multiple_certs(self):
+        cert_value1 = 'cert-value1'
+        cert_value2 = 'cert-value2'
+        cert_value3 = 'cert-value3'
+        certs = [cert_value1, cert_value2, cert_value3]
+        message = object()
+        signature = object()
+
+        verifier = mock.MagicMock()
+        # Use side_effect to force all 3 cert values to be used by failing
+        # to verify on the first two.
+        verifier.verify = mock.MagicMock(name='verify',
+                                         side_effect=[False, False, True])
+        with mock.patch('oauth2client.crypt.Verifier') as Verifier:
+            Verifier.from_string = mock.MagicMock(name='from_string',
+                                                  return_value=verifier)
+            result = crypt._verify_signature(message, signature, certs)
+            self.assertEqual(result, None)
+
+            # Make sure our mocks were called three times.
+            expected_from_string_calls = [
+                mock.call(cert_value1, is_x509_cert=True),
+                mock.call(cert_value2, is_x509_cert=True),
+                mock.call(cert_value3, is_x509_cert=True),
+            ]
+            self.assertEqual(Verifier.from_string.mock_calls,
+                             expected_from_string_calls)
+            expected_verify_calls = [mock.call(message, signature)] * 3
+            self.assertEqual(verifier.verify.mock_calls,
+                             expected_verify_calls)
+
+    def test_failure(self):
+        cert_value = 'cert-value'
+        certs = [cert_value]
+        message = object()
+        signature = object()
+
+        verifier = mock.MagicMock()
+        verifier.verify = mock.MagicMock(name='verify', return_value=False)
+        with mock.patch('oauth2client.crypt.Verifier') as Verifier:
+            Verifier.from_string = mock.MagicMock(name='from_string',
+                                                  return_value=verifier)
+            with self.assertRaises(crypt.AppIdentityError):
+                crypt._verify_signature(message, signature, certs)
+
+            # Make sure our mocks were called as expected.
+            Verifier.from_string.assert_called_once_with(cert_value,
+                                                         is_x509_cert=True)
+            verifier.verify.assert_called_once_with(message, signature)
+
+
+class Test__check_audience(unittest2.TestCase):
+
+    def test_null_audience(self):
+        result = crypt._check_audience(None, None)
+        self.assertEqual(result, None)
+
+    def test_success(self):
+        audience = 'audience'
+        payload_dict = {'aud': audience}
+        result = crypt._check_audience(payload_dict, audience)
+        # No exception and no result.
+        self.assertEqual(result, None)
+
+    def test_missing_aud(self):
+        audience = 'audience'
+        payload_dict = {}
+        with self.assertRaises(crypt.AppIdentityError):
+            crypt._check_audience(payload_dict, audience)
+
+    def test_wrong_aud(self):
+        audience1 = 'audience1'
+        audience2 = 'audience2'
+        self.assertNotEqual(audience1, audience2)
+        payload_dict = {'aud': audience1}
+        with self.assertRaises(crypt.AppIdentityError):
+            crypt._check_audience(payload_dict, audience2)
+
+
+class Test__verify_time_range(unittest2.TestCase):
+
+    def _exception_helper(self, payload_dict):
+        exception_caught = None
+        try:
+            crypt._verify_time_range(payload_dict)
+        except crypt.AppIdentityError as exc:
+            exception_caught = exc
+
+        return exception_caught
+
+    def test_without_issued_at(self):
+        payload_dict = {}
+        exception_caught = self._exception_helper(payload_dict)
+        self.assertNotEqual(exception_caught, None)
+        self.assertTrue(str(exception_caught).startswith(
+            'No iat field in token'))
+
+    def test_without_expiration(self):
+        payload_dict = {'iat': 'iat'}
+        exception_caught = self._exception_helper(payload_dict)
+        self.assertNotEqual(exception_caught, None)
+        self.assertTrue(str(exception_caught).startswith(
+            'No exp field in token'))
+
+    def test_with_bad_token_lifetime(self):
+        current_time = 123456
+        payload_dict = {
+            'iat': 'iat',
+            'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS + 1,
+        }
+        with mock.patch('oauth2client.crypt.time') as time:
+            time.time = mock.MagicMock(name='time',
+                                       return_value=current_time)
+
+            exception_caught = self._exception_helper(payload_dict)
+            self.assertNotEqual(exception_caught, None)
+            self.assertTrue(str(exception_caught).startswith(
+                'exp field too far in future'))
+
+    def test_with_issued_at_in_future(self):
+        current_time = 123456
+        payload_dict = {
+            'iat': current_time + crypt.CLOCK_SKEW_SECS + 1,
+            'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS - 1,
+        }
+        with mock.patch('oauth2client.crypt.time') as time:
+            time.time = mock.MagicMock(name='time',
+                                       return_value=current_time)
+
+            exception_caught = self._exception_helper(payload_dict)
+            self.assertNotEqual(exception_caught, None)
+            self.assertTrue(str(exception_caught).startswith(
+                'Token used too early'))
+
+    def test_with_expiration_in_the_past(self):
+        current_time = 123456
+        payload_dict = {
+            'iat': current_time,
+            'exp': current_time - crypt.CLOCK_SKEW_SECS - 1,
+        }
+        with mock.patch('oauth2client.crypt.time') as time:
+            time.time = mock.MagicMock(name='time',
+                                       return_value=current_time)
+
+            exception_caught = self._exception_helper(payload_dict)
+            self.assertNotEqual(exception_caught, None)
+            self.assertTrue(str(exception_caught).startswith(
+                'Token used too late'))
+
+    def test_success(self):
+        current_time = 123456
+        payload_dict = {
+            'iat': current_time,
+            'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS - 1,
+        }
+        with mock.patch('oauth2client.crypt.time') as time:
+            time.time = mock.MagicMock(name='time',
+                                       return_value=current_time)
+
+            exception_caught = self._exception_helper(payload_dict)
+            self.assertEqual(exception_caught, None)
+
+
+class Test_verify_signed_jwt_with_certs(unittest2.TestCase):
+
+    def test_jwt_no_segments(self):
+        exception_caught = None
+        try:
+            crypt.verify_signed_jwt_with_certs(b'', None)
+        except crypt.AppIdentityError as exc:
+            exception_caught = exc
+
+        self.assertNotEqual(exception_caught, None)
+        self.assertTrue(str(exception_caught).startswith(
+            'Wrong number of segments in token'))
+
+    def test_jwt_payload_bad_json(self):
+        header = signature = b''
+        payload = base64.b64encode(b'{BADJSON')
+        jwt = b'.'.join([header, payload, signature])
+
+        exception_caught = None
+        try:
+            crypt.verify_signed_jwt_with_certs(jwt, None)
+        except crypt.AppIdentityError as exc:
+            exception_caught = exc
+
+        self.assertNotEqual(exception_caught, None)
+        self.assertTrue(str(exception_caught).startswith(
+            'Can\'t parse token'))
+
+    @mock.patch('oauth2client.crypt._check_audience')
+    @mock.patch('oauth2client.crypt._verify_time_range')
+    @mock.patch('oauth2client.crypt._verify_signature')
+    def test_success(self, verify_sig, verify_time, check_aud):
+        certs = mock.MagicMock()
+        cert_values = object()
+        certs.values = mock.MagicMock(name='values',
+                                      return_value=cert_values)
+        audience = object()
+
+        header = b'header'
+        signature_bytes = b'signature'
+        signature = base64.b64encode(signature_bytes)
+        payload_dict = {'a': 'b'}
+        payload = base64.b64encode(b'{"a": "b"}')
+        jwt = b'.'.join([header, payload, signature])
+
+        result = crypt.verify_signed_jwt_with_certs(
+            jwt, certs, audience=audience)
+        self.assertEqual(result, payload_dict)
+
+        message_to_sign = header + b'.' + payload
+        verify_sig.assert_called_once_with(
+            message_to_sign, signature_bytes, cert_values)
+        verify_time.assert_called_once_with(payload_dict)
+        check_aud.assert_called_once_with(payload_dict, audience)
+        certs.values.assert_called_once_with()
diff --git a/tests/test_file.py b/tests/test_file.py
new file mode 100644
index 0000000..924acb4
--- /dev/null
+++ b/tests/test_file.py
@@ -0,0 +1,243 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Oauth2client.file tests
+
+Unit tests for oauth2client.file
+"""
+
+import copy
+import datetime
+import json
+import os
+import pickle
+import stat
+import tempfile
+
+import six
+from six.moves import http_client
+import unittest2
+
+from oauth2client import client
+from oauth2client import file
+from .http_mock import HttpMockSequence
+
+try:
+    # Python2
+    from future_builtins import oct
+except:  # pragma: NO COVER
+    pass
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+_filehandle, FILENAME = tempfile.mkstemp('oauth2client_test.data')
+os.close(_filehandle)
+
+
+class OAuth2ClientFileTests(unittest2.TestCase):
+
+    def tearDown(self):
+        try:
+            os.unlink(FILENAME)
+        except OSError:
+            pass
+
+    def setUp(self):
+        try:
+            os.unlink(FILENAME)
+        except OSError:
+            pass
+
+    def _create_test_credentials(self, client_id='some_client_id',
+                                 expiration=None):
+        access_token = 'foo'
+        client_secret = 'cOuDdkfjxxnv+'
+        refresh_token = '1/0/a.df219fjls0'
+        token_expiry = expiration or datetime.datetime.utcnow()
+        token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
+        user_agent = 'refresh_checker/1.0'
+
+        credentials = client.OAuth2Credentials(
+            access_token, client_id, client_secret,
+            refresh_token, token_expiry, token_uri,
+            user_agent)
+        return credentials
+
+    def test_non_existent_file_storage(self):
+        s = file.Storage(FILENAME)
+        credentials = s.get()
+        self.assertEquals(None, credentials)
+
+    @unittest2.skipIf(not hasattr(os, 'symlink'), 'No symlink available')
+    def test_no_sym_link_credentials(self):
+        SYMFILENAME = FILENAME + '.sym'
+        os.symlink(FILENAME, SYMFILENAME)
+        s = file.Storage(SYMFILENAME)
+        try:
+            with self.assertRaises(file.CredentialsFileSymbolicLinkError):
+                s.get()
+        finally:
+            os.unlink(SYMFILENAME)
+
+    def test_pickle_and_json_interop(self):
+        # Write a file with a pickled OAuth2Credentials.
+        credentials = self._create_test_credentials()
+
+        f = open(FILENAME, 'wb')
+        pickle.dump(credentials, f)
+        f.close()
+
+        # Storage should be not be able to read that object, as the capability
+        # to read and write credentials as pickled objects has been removed.
+        s = file.Storage(FILENAME)
+        read_credentials = s.get()
+        self.assertEquals(None, read_credentials)
+
+        # Now write it back out and confirm it has been rewritten as JSON
+        s.put(credentials)
+        with open(FILENAME) as f:
+            data = json.load(f)
+
+        self.assertEquals(data['access_token'], 'foo')
+        self.assertEquals(data['_class'], 'OAuth2Credentials')
+        self.assertEquals(data['_module'], client.OAuth2Credentials.__module__)
+
+    def test_token_refresh_store_expired(self):
+        expiration = (datetime.datetime.utcnow() -
+                      datetime.timedelta(minutes=15))
+        credentials = self._create_test_credentials(expiration=expiration)
+
+        s = file.Storage(FILENAME)
+        s.put(credentials)
+        credentials = s.get()
+        new_cred = copy.copy(credentials)
+        new_cred.access_token = 'bar'
+        s.put(new_cred)
+
+        access_token = '1/3w'
+        token_response = {'access_token': access_token, 'expires_in': 3600}
+        http = HttpMockSequence([
+            ({'status': '200'}, json.dumps(token_response).encode('utf-8')),
+        ])
+
+        credentials._refresh(http.request)
+        self.assertEquals(credentials.access_token, access_token)
+
+    def test_token_refresh_store_expires_soon(self):
+        # Tests the case where an access token that is valid when it is read
+        # from the store expires before the original request succeeds.
+        expiration = (datetime.datetime.utcnow() +
+                      datetime.timedelta(minutes=15))
+        credentials = self._create_test_credentials(expiration=expiration)
+
+        s = file.Storage(FILENAME)
+        s.put(credentials)
+        credentials = s.get()
+        new_cred = copy.copy(credentials)
+        new_cred.access_token = 'bar'
+        s.put(new_cred)
+
+        access_token = '1/3w'
+        token_response = {'access_token': access_token, 'expires_in': 3600}
+        http = HttpMockSequence([
+            ({'status': str(int(http_client.UNAUTHORIZED))},
+             b'Initial token expired'),
+            ({'status': str(int(http_client.UNAUTHORIZED))},
+             b'Store token expired'),
+            ({'status': str(int(http_client.OK))},
+             json.dumps(token_response).encode('utf-8')),
+            ({'status': str(int(http_client.OK))},
+             b'Valid response to original request')
+        ])
+
+        credentials.authorize(http)
+        http.request('https://example.com')
+        self.assertEqual(credentials.access_token, access_token)
+
+    def test_token_refresh_good_store(self):
+        expiration = (datetime.datetime.utcnow() +
+                      datetime.timedelta(minutes=15))
+        credentials = self._create_test_credentials(expiration=expiration)
+
+        s = file.Storage(FILENAME)
+        s.put(credentials)
+        credentials = s.get()
+        new_cred = copy.copy(credentials)
+        new_cred.access_token = 'bar'
+        s.put(new_cred)
+
+        credentials._refresh(None)
+        self.assertEquals(credentials.access_token, 'bar')
+
+    def test_token_refresh_stream_body(self):
+        expiration = (datetime.datetime.utcnow() +
+                      datetime.timedelta(minutes=15))
+        credentials = self._create_test_credentials(expiration=expiration)
+
+        s = file.Storage(FILENAME)
+        s.put(credentials)
+        credentials = s.get()
+        new_cred = copy.copy(credentials)
+        new_cred.access_token = 'bar'
+        s.put(new_cred)
+
+        valid_access_token = '1/3w'
+        token_response = {'access_token': valid_access_token,
+                          'expires_in': 3600}
+        http = HttpMockSequence([
+            ({'status': str(int(http_client.UNAUTHORIZED))},
+             b'Initial token expired'),
+            ({'status': str(int(http_client.UNAUTHORIZED))},
+             b'Store token expired'),
+            ({'status': str(int(http_client.OK))},
+             json.dumps(token_response).encode('utf-8')),
+            ({'status': str(int(http_client.OK))}, 'echo_request_body')
+        ])
+
+        body = six.StringIO('streaming body')
+
+        credentials.authorize(http)
+        _, content = http.request('https://example.com', body=body)
+        self.assertEqual(content, 'streaming body')
+        self.assertEqual(credentials.access_token, valid_access_token)
+
+    def test_credentials_delete(self):
+        credentials = self._create_test_credentials()
+
+        s = file.Storage(FILENAME)
+        s.put(credentials)
+        credentials = s.get()
+        self.assertNotEquals(None, credentials)
+        s.delete()
+        credentials = s.get()
+        self.assertEquals(None, credentials)
+
+    def test_access_token_credentials(self):
+        access_token = 'foo'
+        user_agent = 'refresh_checker/1.0'
+
+        credentials = client.AccessTokenCredentials(access_token, user_agent)
+
+        s = file.Storage(FILENAME)
+        credentials = s.put(credentials)
+        credentials = s.get()
+
+        self.assertNotEquals(None, credentials)
+        self.assertEquals('foo', credentials.access_token)
+
+        self.assertTrue(os.path.exists(FILENAME))
+
+        if os.name == 'posix':  # pragma: NO COVER
+            mode = os.stat(FILENAME).st_mode
+            self.assertEquals('0o600', oct(stat.S_IMODE(mode)))
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
new file mode 100644
index 0000000..ecc58e8
--- /dev/null
+++ b/tests/test_jwt.py
@@ -0,0 +1,329 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Unit tests for JWT related methods in oauth2client."""
+
+import os
+import tempfile
+import time
+
+import mock
+import unittest2
+
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import crypt
+from oauth2client import file
+from oauth2client import service_account
+from .http_mock import HttpMockSequence
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+_FORMATS_TO_CONSTRUCTOR_ARGS = {
+    'p12': 'private_key_pkcs12',
+    'pem': 'private_key_pkcs8_pem',
+}
+
+
+def data_filename(filename):
+    return os.path.join(os.path.dirname(__file__), 'data', filename)
+
+
+def datafile(filename):
+    with open(data_filename(filename), 'rb') as file_obj:
+        return file_obj.read()
+
+
+class CryptTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.format_ = 'p12'
+        self.signer = crypt.OpenSSLSigner
+        self.verifier = crypt.OpenSSLVerifier
+
+    def test_sign_and_verify(self):
+        self._check_sign_and_verify('privatekey.' + self.format_)
+
+    def test_sign_and_verify_from_converted_pkcs12(self):
+        # Tests that following instructions to convert from PKCS12 to
+        # PEM works.
+        if self.format_ == 'pem':
+            self._check_sign_and_verify('pem_from_pkcs12.pem')
+
+    def _check_sign_and_verify(self, private_key_file):
+        private_key = datafile(private_key_file)
+        public_key = datafile('public_cert.pem')
+
+        # We pass in a non-bytes password to make sure all branches
+        # are traversed in tests.
+        signer = self.signer.from_string(private_key,
+                                         password=u'notasecret')
+        signature = signer.sign('foo')
+
+        verifier = self.verifier.from_string(public_key, True)
+        self.assertTrue(verifier.verify(b'foo', signature))
+
+        self.assertFalse(verifier.verify(b'bar', signature))
+        self.assertFalse(verifier.verify(b'foo', b'bad signagure'))
+        self.assertFalse(verifier.verify(b'foo', u'bad signagure'))
+
+    def _check_jwt_failure(self, jwt, expected_error):
+        public_key = datafile('public_cert.pem')
+        certs = {'foo': public_key}
+        audience = ('https://www.googleapis.com/auth/id?client_id='
+                    'external_public_key@testing.gserviceaccount.com')
+
+        with self.assertRaises(crypt.AppIdentityError) as exc_manager:
+            crypt.verify_signed_jwt_with_certs(jwt, certs, audience)
+
+        self.assertTrue(expected_error in str(exc_manager.exception))
+
+    def _create_signed_jwt(self):
+        private_key = datafile('privatekey.' + self.format_)
+        signer = self.signer.from_string(private_key)
+        audience = 'some_audience_address@testing.gserviceaccount.com'
+        now = int(time.time())
+
+        return crypt.make_signed_jwt(signer, {
+            'aud': audience,
+            'iat': now,
+            'exp': now + 300,
+            'user': 'billy bob',
+            'metadata': {'meta': 'data'},
+        })
+
+    def test_verify_id_token(self):
+        jwt = self._create_signed_jwt()
+        public_key = datafile('public_cert.pem')
+        certs = {'foo': public_key}
+        audience = 'some_audience_address@testing.gserviceaccount.com'
+        contents = crypt.verify_signed_jwt_with_certs(jwt, certs, audience)
+        self.assertEqual('billy bob', contents['user'])
+        self.assertEqual('data', contents['metadata']['meta'])
+
+    def test_verify_id_token_with_certs_uri(self):
+        jwt = self._create_signed_jwt()
+
+        http = HttpMockSequence([
+            ({'status': '200'}, datafile('certs.json')),
+        ])
+
+        contents = client.verify_id_token(
+            jwt, 'some_audience_address@testing.gserviceaccount.com',
+            http=http)
+        self.assertEqual('billy bob', contents['user'])
+        self.assertEqual('data', contents['metadata']['meta'])
+
+    def test_verify_id_token_with_certs_uri_default_http(self):
+        jwt = self._create_signed_jwt()
+
+        http = HttpMockSequence([
+            ({'status': '200'}, datafile('certs.json')),
+        ])
+
+        with mock.patch('oauth2client.transport._CACHED_HTTP', new=http):
+            contents = client.verify_id_token(
+                jwt, 'some_audience_address@testing.gserviceaccount.com')
+
+        self.assertEqual('billy bob', contents['user'])
+        self.assertEqual('data', contents['metadata']['meta'])
+
+    def test_verify_id_token_with_certs_uri_fails(self):
+        jwt = self._create_signed_jwt()
+        test_email = 'some_audience_address@testing.gserviceaccount.com'
+
+        http = HttpMockSequence([
+            ({'status': '404'}, datafile('certs.json')),
+        ])
+
+        with self.assertRaises(client.VerifyJwtTokenError):
+            client.verify_id_token(jwt, test_email, http=http)
+
+    def test_verify_id_token_bad_tokens(self):
+        private_key = datafile('privatekey.' + self.format_)
+
+        # Wrong number of segments
+        self._check_jwt_failure('foo', 'Wrong number of segments')
+
+        # Not json
+        self._check_jwt_failure('foo.bar.baz', 'Can\'t parse token')
+
+        # Bad signature
+        jwt = b'.'.join([b'foo',
+                         _helpers._urlsafe_b64encode('{"a":"b"}'),
+                         b'baz'])
+        self._check_jwt_failure(jwt, 'Invalid token signature')
+
+        # No expiration
+        signer = self.signer.from_string(private_key)
+        audience = ('https:#www.googleapis.com/auth/id?client_id='
+                    'external_public_key@testing.gserviceaccount.com')
+        jwt = crypt.make_signed_jwt(signer, {
+            'aud': audience,
+            'iat': time.time(),
+        })
+        self._check_jwt_failure(jwt, 'No exp field in token')
+
+        # No issued at
+        jwt = crypt.make_signed_jwt(signer, {
+            'aud': 'audience',
+            'exp': time.time() + 400,
+        })
+        self._check_jwt_failure(jwt, 'No iat field in token')
+
+        # Too early
+        jwt = crypt.make_signed_jwt(signer, {
+            'aud': 'audience',
+            'iat': time.time() + 301,
+            'exp': time.time() + 400,
+        })
+        self._check_jwt_failure(jwt, 'Token used too early')
+
+        # Too late
+        jwt = crypt.make_signed_jwt(signer, {
+            'aud': 'audience',
+            'iat': time.time() - 500,
+            'exp': time.time() - 301,
+        })
+        self._check_jwt_failure(jwt, 'Token used too late')
+
+        # Wrong target
+        jwt = crypt.make_signed_jwt(signer, {
+            'aud': 'somebody else',
+            'iat': time.time(),
+            'exp': time.time() + 300,
+        })
+        self._check_jwt_failure(jwt, 'Wrong recipient')
+
+    def test_from_string_non_509_cert(self):
+        # Use a private key instead of a certificate to test the other branch
+        # of from_string().
+        public_key = datafile('privatekey.pem')
+        verifier = self.verifier.from_string(public_key, is_x509_cert=False)
+        self.assertIsInstance(verifier, self.verifier)
+
+
+class PEMCryptTestsPyCrypto(CryptTests):
+
+    def setUp(self):
+        self.format_ = 'pem'
+        self.signer = crypt.PyCryptoSigner
+        self.verifier = crypt.PyCryptoVerifier
+
+
+class PEMCryptTestsOpenSSL(CryptTests):
+
+    def setUp(self):
+        self.format_ = 'pem'
+        self.signer = crypt.OpenSSLSigner
+        self.verifier = crypt.OpenSSLVerifier
+
+
+class SignedJwtAssertionCredentialsTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.format_ = 'p12'
+        crypt.Signer = crypt.OpenSSLSigner
+
+    def _make_credentials(self):
+        private_key = datafile('privatekey.' + self.format_)
+        signer = crypt.Signer.from_string(private_key)
+        credentials = service_account.ServiceAccountCredentials(
+            'some_account@example.com', signer,
+            scopes='read+write',
+            sub='joe@example.org')
+        if self.format_ == 'pem':
+            credentials._private_key_pkcs8_pem = private_key
+        elif self.format_ == 'p12':
+            credentials._private_key_pkcs12 = private_key
+            credentials._private_key_password = (
+                service_account._PASSWORD_DEFAULT)
+        else:  # pragma: NO COVER
+            raise ValueError('Unexpected format.')
+        return credentials
+
+    def test_credentials_good(self):
+        credentials = self._make_credentials()
+        http = HttpMockSequence([
+            ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
+            ({'status': '200'}, 'echo_request_headers'),
+        ])
+        http = credentials.authorize(http)
+        resp, content = http.request('http://example.org')
+        self.assertEqual(b'Bearer 1/3w', content[b'Authorization'])
+
+    def test_credentials_to_from_json(self):
+        credentials = self._make_credentials()
+        json = credentials.to_json()
+        restored = client.Credentials.new_from_json(json)
+        self.assertEqual(credentials._private_key_pkcs12,
+                         restored._private_key_pkcs12)
+        self.assertEqual(credentials._private_key_password,
+                         restored._private_key_password)
+        self.assertEqual(credentials._kwargs, restored._kwargs)
+
+    def _credentials_refresh(self, credentials):
+        http = HttpMockSequence([
+            ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
+            ({'status': '401'}, b''),
+            ({'status': '200'}, b'{"access_token":"3/3w","expires_in":3600}'),
+            ({'status': '200'}, 'echo_request_headers'),
+        ])
+        http = credentials.authorize(http)
+        _, content = http.request('http://example.org')
+        return content
+
+    def test_credentials_refresh_without_storage(self):
+        credentials = self._make_credentials()
+        content = self._credentials_refresh(credentials)
+        self.assertEqual(b'Bearer 3/3w', content[b'Authorization'])
+
+    def test_credentials_refresh_with_storage(self):
+        credentials = self._make_credentials()
+
+        filehandle, filename = tempfile.mkstemp()
+        os.close(filehandle)
+        store = file.Storage(filename)
+        store.put(credentials)
+        credentials.set_store(store)
+
+        content = self._credentials_refresh(credentials)
+
+        self.assertEqual(b'Bearer 3/3w', content[b'Authorization'])
+        os.unlink(filename)
+
+
+class PEMSignedJwtAssertionCredentialsOpenSSLTests(
+        SignedJwtAssertionCredentialsTests):
+
+    def setUp(self):
+        self.format_ = 'pem'
+        crypt.Signer = crypt.OpenSSLSigner
+
+
+class PEMSignedJwtAssertionCredentialsPyCryptoTests(
+        SignedJwtAssertionCredentialsTests):
+
+    def setUp(self):
+        self.format_ = 'pem'
+        crypt.Signer = crypt.PyCryptoSigner
+
+
+class TestHasOpenSSLFlag(unittest2.TestCase):
+
+    def test_true(self):
+        self.assertEqual(True, client.HAS_OPENSSL)
+        self.assertEqual(True, client.HAS_CRYPTO)
diff --git a/tests/test_service_account.py b/tests/test_service_account.py
new file mode 100644
index 0000000..699e699
--- /dev/null
+++ b/tests/test_service_account.py
@@ -0,0 +1,580 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Oauth2client tests.
+
+Unit tests for service account credentials implemented using RSA.
+"""
+
+import datetime
+import json
+import os
+import tempfile
+
+import httplib2
+import mock
+import rsa
+from six import BytesIO
+import unittest2
+
+from oauth2client import client
+from oauth2client import crypt
+from oauth2client import service_account
+from .http_mock import HttpMockSequence
+
+
+def data_filename(filename):
+    return os.path.join(os.path.dirname(__file__), 'data', filename)
+
+
+def datafile(filename):
+    with open(data_filename(filename), 'rb') as file_obj:
+        return file_obj.read()
+
+
+class ServiceAccountCredentialsTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.client_id = '123'
+        self.service_account_email = 'dummy@google.com'
+        self.private_key_id = 'ABCDEF'
+        self.private_key = datafile('pem_from_pkcs12.pem')
+        self.scopes = ['dummy_scope']
+        self.signer = crypt.Signer.from_string(self.private_key)
+        self.credentials = service_account.ServiceAccountCredentials(
+            self.service_account_email,
+            self.signer,
+            private_key_id=self.private_key_id,
+            client_id=self.client_id,
+        )
+
+    def test__to_json_override(self):
+        signer = object()
+        creds = service_account.ServiceAccountCredentials(
+            'name@email.com', signer)
+        self.assertEqual(creds._signer, signer)
+        # Serialize over-ridden data (unrelated to ``creds``).
+        to_serialize = {'unrelated': 'data'}
+        serialized_str = creds._to_json([], to_serialize.copy())
+        serialized_data = json.loads(serialized_str)
+        expected_serialized = {
+            '_class': 'ServiceAccountCredentials',
+            '_module': 'oauth2client.service_account',
+            'token_expiry': None,
+        }
+        expected_serialized.update(to_serialize)
+        self.assertEqual(serialized_data, expected_serialized)
+
+    def test_sign_blob(self):
+        private_key_id, signature = self.credentials.sign_blob('Google')
+        self.assertEqual(self.private_key_id, private_key_id)
+
+        pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(
+            datafile('publickey_openssl.pem'))
+
+        self.assertTrue(rsa.pkcs1.verify(b'Google', signature, pub_key))
+
+        with self.assertRaises(rsa.pkcs1.VerificationError):
+            rsa.pkcs1.verify(b'Orest', signature, pub_key)
+        with self.assertRaises(rsa.pkcs1.VerificationError):
+            rsa.pkcs1.verify(b'Google', b'bad signature', pub_key)
+
+    def test_service_account_email(self):
+        self.assertEqual(self.service_account_email,
+                         self.credentials.service_account_email)
+
+    @staticmethod
+    def _from_json_keyfile_name_helper(payload, scopes=None,
+                                       token_uri=None, revoke_uri=None):
+        filehandle, filename = tempfile.mkstemp()
+        os.close(filehandle)
+        try:
+            with open(filename, 'w') as file_obj:
+                json.dump(payload, file_obj)
+            return (
+                service_account.ServiceAccountCredentials
+                .from_json_keyfile_name(
+                    filename, scopes=scopes, token_uri=token_uri,
+                    revoke_uri=revoke_uri))
+        finally:
+            os.remove(filename)
+
+    @mock.patch('oauth2client.crypt.Signer.from_string',
+                return_value=object())
+    def test_from_json_keyfile_name_factory(self, signer_factory):
+        client_id = 'id123'
+        client_email = 'foo@bar.com'
+        private_key_id = 'pkid456'
+        private_key = 's3kr3tz'
+        payload = {
+            'type': client.SERVICE_ACCOUNT,
+            'client_id': client_id,
+            'client_email': client_email,
+            'private_key_id': private_key_id,
+            'private_key': private_key,
+        }
+        scopes = ['foo', 'bar']
+        token_uri = 'baz'
+        revoke_uri = 'qux'
+        base_creds = self._from_json_keyfile_name_helper(
+            payload, scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri)
+        self.assertEqual(base_creds._signer, signer_factory.return_value)
+        signer_factory.assert_called_once_with(private_key)
+
+        payload['token_uri'] = token_uri
+        payload['revoke_uri'] = revoke_uri
+        creds_with_uris_from_file = self._from_json_keyfile_name_helper(
+            payload, scopes=scopes)
+        for creds in (base_creds, creds_with_uris_from_file):
+            self.assertIsInstance(
+                creds, service_account.ServiceAccountCredentials)
+            self.assertEqual(creds.client_id, client_id)
+            self.assertEqual(creds._service_account_email, client_email)
+            self.assertEqual(creds._private_key_id, private_key_id)
+            self.assertEqual(creds._private_key_pkcs8_pem, private_key)
+            self.assertEqual(creds._scopes, ' '.join(scopes))
+            self.assertEqual(creds.token_uri, token_uri)
+            self.assertEqual(creds.revoke_uri, revoke_uri)
+
+    def test_from_json_keyfile_name_factory_bad_type(self):
+        type_ = 'bad-type'
+        self.assertNotEqual(type_, client.SERVICE_ACCOUNT)
+        payload = {'type': type_}
+        with self.assertRaises(ValueError):
+            self._from_json_keyfile_name_helper(payload)
+
+    def test_from_json_keyfile_name_factory_missing_field(self):
+        payload = {
+            'type': client.SERVICE_ACCOUNT,
+            'client_id': 'my-client',
+        }
+        with self.assertRaises(KeyError):
+            self._from_json_keyfile_name_helper(payload)
+
+    def _from_p12_keyfile_helper(self, private_key_password=None, scopes='',
+                                 token_uri=None, revoke_uri=None):
+        service_account_email = 'name@email.com'
+        filename = data_filename('privatekey.p12')
+        with open(filename, 'rb') as file_obj:
+            key_contents = file_obj.read()
+        creds_from_filename = (
+            service_account.ServiceAccountCredentials.from_p12_keyfile(
+                service_account_email, filename,
+                private_key_password=private_key_password,
+                scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri))
+        creds_from_file_contents = (
+            service_account.ServiceAccountCredentials.from_p12_keyfile_buffer(
+                service_account_email, BytesIO(key_contents),
+                private_key_password=private_key_password,
+                scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri))
+        for creds in (creds_from_filename, creds_from_file_contents):
+            self.assertIsInstance(
+                creds, service_account.ServiceAccountCredentials)
+            self.assertIsNone(creds.client_id)
+            self.assertEqual(creds._service_account_email,
+                             service_account_email)
+            self.assertIsNone(creds._private_key_id)
+            self.assertIsNone(creds._private_key_pkcs8_pem)
+            self.assertEqual(creds._private_key_pkcs12, key_contents)
+            if private_key_password is not None:
+                self.assertEqual(creds._private_key_password,
+                                 private_key_password)
+            self.assertEqual(creds._scopes, ' '.join(scopes))
+            self.assertEqual(creds.token_uri, token_uri)
+            self.assertEqual(creds.revoke_uri, revoke_uri)
+
+    def _p12_not_implemented_helper(self):
+        service_account_email = 'name@email.com'
+        filename = data_filename('privatekey.p12')
+        with self.assertRaises(NotImplementedError):
+            service_account.ServiceAccountCredentials.from_p12_keyfile(
+                service_account_email, filename)
+
+    @mock.patch('oauth2client.crypt.Signer', new=crypt.PyCryptoSigner)
+    def test_from_p12_keyfile_with_pycrypto(self):
+        self._p12_not_implemented_helper()
+
+    @mock.patch('oauth2client.crypt.Signer', new=crypt.RsaSigner)
+    def test_from_p12_keyfile_with_rsa(self):
+        self._p12_not_implemented_helper()
+
+    def test_from_p12_keyfile_defaults(self):
+        self._from_p12_keyfile_helper()
+
+    def test_from_p12_keyfile_explicit(self):
+        password = 'notasecret'
+        self._from_p12_keyfile_helper(private_key_password=password,
+                                      scopes=['foo', 'bar'],
+                                      token_uri='baz', revoke_uri='qux')
+
+    def test_create_scoped_required_without_scopes(self):
+        self.assertTrue(self.credentials.create_scoped_required())
+
+    def test_create_scoped_required_with_scopes(self):
+        signer = object()
+        self.credentials = service_account.ServiceAccountCredentials(
+            self.service_account_email,
+            signer,
+            scopes=self.scopes,
+            private_key_id=self.private_key_id,
+            client_id=self.client_id,
+        )
+        self.assertFalse(self.credentials.create_scoped_required())
+
+    def test_create_scoped(self):
+        new_credentials = self.credentials.create_scoped(self.scopes)
+        self.assertNotEqual(self.credentials, new_credentials)
+        self.assertIsInstance(new_credentials,
+                              service_account.ServiceAccountCredentials)
+        self.assertEqual('dummy_scope', new_credentials._scopes)
+
+    def test_create_delegated(self):
+        signer = object()
+        sub = 'foo@email.com'
+        creds = service_account.ServiceAccountCredentials(
+            'name@email.com', signer)
+        self.assertNotIn('sub', creds._kwargs)
+        delegated_creds = creds.create_delegated(sub)
+        self.assertEqual(delegated_creds._kwargs['sub'], sub)
+        # Make sure the original is unchanged.
+        self.assertNotIn('sub', creds._kwargs)
+
+    def test_create_delegated_existing_sub(self):
+        signer = object()
+        sub1 = 'existing@email.com'
+        sub2 = 'new@email.com'
+        creds = service_account.ServiceAccountCredentials(
+            'name@email.com', signer, sub=sub1)
+        self.assertEqual(creds._kwargs['sub'], sub1)
+        delegated_creds = creds.create_delegated(sub2)
+        self.assertEqual(delegated_creds._kwargs['sub'], sub2)
+        # Make sure the original is unchanged.
+        self.assertEqual(creds._kwargs['sub'], sub1)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_access_token(self, utcnow):
+        # Configure the patch.
+        seconds = 11
+        NOW = datetime.datetime(1992, 12, 31, second=seconds)
+        utcnow.return_value = NOW
+
+        # Create a custom credentials with a mock signer.
+        signer = mock.MagicMock()
+        signed_value = b'signed-content'
+        signer.sign = mock.MagicMock(name='sign',
+                                     return_value=signed_value)
+        credentials = service_account.ServiceAccountCredentials(
+            self.service_account_email,
+            signer,
+            private_key_id=self.private_key_id,
+            client_id=self.client_id,
+        )
+
+        # Begin testing.
+        lifetime = 2  # number of seconds in which the token expires
+        EXPIRY_TIME = datetime.datetime(1992, 12, 31,
+                                        second=seconds + lifetime)
+
+        token1 = u'first_token'
+        token_response_first = {
+            'access_token': token1,
+            'expires_in': lifetime,
+        }
+        token2 = u'second_token'
+        token_response_second = {
+            'access_token': token2,
+            'expires_in': lifetime,
+        }
+        http = HttpMockSequence([
+            ({'status': '200'},
+             json.dumps(token_response_first).encode('utf-8')),
+            ({'status': '200'},
+             json.dumps(token_response_second).encode('utf-8')),
+        ])
+
+        # Get Access Token, First attempt.
+        self.assertIsNone(credentials.access_token)
+        self.assertFalse(credentials.access_token_expired)
+        self.assertIsNone(credentials.token_expiry)
+        token = credentials.get_access_token(http=http)
+        self.assertEqual(credentials.token_expiry, EXPIRY_TIME)
+        self.assertEqual(token1, token.access_token)
+        self.assertEqual(lifetime, token.expires_in)
+        self.assertEqual(token_response_first,
+                         credentials.token_response)
+        # Two utcnow calls are expected:
+        # - get_access_token() -> _do_refresh_request (setting expires in)
+        # - get_access_token() -> _expires_in()
+        expected_utcnow_calls = [mock.call()] * 2
+        self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
+        # One call to sign() expected: Actual refresh was needed.
+        self.assertEqual(len(signer.sign.mock_calls), 1)
+
+        # Get Access Token, Second Attempt (not expired)
+        self.assertEqual(credentials.access_token, token1)
+        self.assertFalse(credentials.access_token_expired)
+        token = credentials.get_access_token(http=http)
+        # Make sure no refresh occurred since the token was not expired.
+        self.assertEqual(token1, token.access_token)
+        self.assertEqual(lifetime, token.expires_in)
+        self.assertEqual(token_response_first, credentials.token_response)
+        # Three more utcnow calls are expected:
+        # - access_token_expired
+        # - get_access_token() -> access_token_expired
+        # - get_access_token -> _expires_in
+        expected_utcnow_calls = [mock.call()] * (2 + 3)
+        self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
+        # No call to sign() expected: the token was not expired.
+        self.assertEqual(len(signer.sign.mock_calls), 1 + 0)
+
+        # Get Access Token, Third Attempt (force expiration)
+        self.assertEqual(credentials.access_token, token1)
+        credentials.token_expiry = NOW  # Manually force expiry.
+        self.assertTrue(credentials.access_token_expired)
+        token = credentials.get_access_token(http=http)
+        # Make sure refresh occurred since the token was not expired.
+        self.assertEqual(token2, token.access_token)
+        self.assertEqual(lifetime, token.expires_in)
+        self.assertFalse(credentials.access_token_expired)
+        self.assertEqual(token_response_second,
+                         credentials.token_response)
+        # Five more utcnow calls are expected:
+        # - access_token_expired
+        # - get_access_token -> access_token_expired
+        # - get_access_token -> _do_refresh_request
+        # - get_access_token -> _expires_in
+        # - access_token_expired
+        expected_utcnow_calls = [mock.call()] * (2 + 3 + 5)
+        self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
+        # One more call to sign() expected: Actual refresh was needed.
+        self.assertEqual(len(signer.sign.mock_calls), 1 + 0 + 1)
+
+        self.assertEqual(credentials.access_token, token2)
+
+TOKEN_LIFE = service_account._JWTAccessCredentials._MAX_TOKEN_LIFETIME_SECS
+T1 = 42
+T1_DATE = datetime.datetime(1970, 1, 1, second=T1)
+T1_EXPIRY = T1 + TOKEN_LIFE
+T1_EXPIRY_DATE = T1_DATE + datetime.timedelta(seconds=TOKEN_LIFE)
+
+T2 = T1 + 100
+T2_DATE = T1_DATE + datetime.timedelta(seconds=100)
+T2_EXPIRY = T2 + TOKEN_LIFE
+T2_EXPIRY_DATE = T2_DATE + datetime.timedelta(seconds=TOKEN_LIFE)
+
+T3 = T1 + TOKEN_LIFE + 1
+T3_DATE = T1_DATE + datetime.timedelta(seconds=TOKEN_LIFE + 1)
+T3_EXPIRY = T3 + TOKEN_LIFE
+T3_EXPIRY_DATE = T3_DATE + datetime.timedelta(seconds=TOKEN_LIFE)
+
+
+class JWTAccessCredentialsTests(unittest2.TestCase):
+
+    def setUp(self):
+        self.client_id = '123'
+        self.service_account_email = 'dummy@google.com'
+        self.private_key_id = 'ABCDEF'
+        self.private_key = datafile('pem_from_pkcs12.pem')
+        self.signer = crypt.Signer.from_string(self.private_key)
+        self.url = 'https://test.url.com'
+        self.jwt = service_account._JWTAccessCredentials(
+            self.service_account_email, self.signer,
+            private_key_id=self.private_key_id, client_id=self.client_id,
+            additional_claims={'aud': self.url})
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    @mock.patch('time.time')
+    def test_get_access_token_no_claims(self, time, utcnow):
+        utcnow.return_value = T1_DATE
+        time.return_value = T1
+
+        token_info = self.jwt.get_access_token()
+        payload = crypt.verify_signed_jwt_with_certs(
+            token_info.access_token,
+            {'key': datafile('public_cert.pem')}, audience=self.url)
+        self.assertEqual(payload['iss'], self.service_account_email)
+        self.assertEqual(payload['sub'], self.service_account_email)
+        self.assertEqual(payload['iat'], T1)
+        self.assertEqual(payload['exp'], T1_EXPIRY)
+        self.assertEqual(token_info.expires_in, T1_EXPIRY - T1)
+
+        # Verify that we vend the same token after 100 seconds
+        utcnow.return_value = T2_DATE
+        token_info = self.jwt.get_access_token()
+        payload = crypt.verify_signed_jwt_with_certs(
+            token_info.access_token,
+            {'key': datafile('public_cert.pem')}, audience=self.url)
+        self.assertEqual(payload['iat'], T1)
+        self.assertEqual(payload['exp'], T1_EXPIRY)
+        self.assertEqual(token_info.expires_in, T1_EXPIRY - T2)
+
+        # Verify that we vend a new token after _MAX_TOKEN_LIFETIME_SECS
+        utcnow.return_value = T3_DATE
+        time.return_value = T3
+        token_info = self.jwt.get_access_token()
+        payload = crypt.verify_signed_jwt_with_certs(
+            token_info.access_token,
+            {'key': datafile('public_cert.pem')}, audience=self.url)
+        expires_in = token_info.expires_in
+        self.assertEqual(payload['iat'], T3)
+        self.assertEqual(payload['exp'], T3_EXPIRY)
+        self.assertEqual(expires_in, T3_EXPIRY - T3)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    @mock.patch('time.time')
+    def test_get_access_token_additional_claims(self, time, utcnow):
+        utcnow.return_value = T1_DATE
+        time.return_value = T1
+
+        token_info = self.jwt.get_access_token(
+            additional_claims={'aud': 'https://test2.url.com',
+                               'sub': 'dummy2@google.com'
+                               })
+        payload = crypt.verify_signed_jwt_with_certs(
+            token_info.access_token,
+            {'key': datafile('public_cert.pem')},
+            audience='https://test2.url.com')
+        expires_in = token_info.expires_in
+        self.assertEqual(payload['iss'], self.service_account_email)
+        self.assertEqual(payload['sub'], 'dummy2@google.com')
+        self.assertEqual(payload['iat'], T1)
+        self.assertEqual(payload['exp'], T1_EXPIRY)
+        self.assertEqual(expires_in, T1_EXPIRY - T1)
+
+    def test_revoke(self):
+        self.jwt.revoke(None)
+
+    def test_create_scoped_required(self):
+        self.assertTrue(self.jwt.create_scoped_required())
+
+    def test_create_scoped(self):
+        self.jwt._private_key_pkcs12 = ''
+        self.jwt._private_key_password = ''
+
+        new_credentials = self.jwt.create_scoped('dummy_scope')
+        self.assertNotEqual(self.jwt, new_credentials)
+        self.assertIsInstance(
+            new_credentials, service_account.ServiceAccountCredentials)
+        self.assertEqual('dummy_scope', new_credentials._scopes)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    @mock.patch('time.time')
+    def test_authorize_success(self, time, utcnow):
+        utcnow.return_value = T1_DATE
+        time.return_value = T1
+
+        def mock_request(uri, method='GET', body=None, headers=None,
+                         redirections=0, connection_type=None):
+            self.assertEqual(uri, self.url)
+            bearer, token = headers[b'Authorization'].split()
+            payload = crypt.verify_signed_jwt_with_certs(
+                token,
+                {'key': datafile('public_cert.pem')},
+                audience=self.url)
+            self.assertEqual(payload['iss'], self.service_account_email)
+            self.assertEqual(payload['sub'], self.service_account_email)
+            self.assertEqual(payload['iat'], T1)
+            self.assertEqual(payload['exp'], T1_EXPIRY)
+            self.assertEqual(uri, self.url)
+            self.assertEqual(bearer, b'Bearer')
+            return (httplib2.Response({'status': '200'}), b'')
+
+        h = httplib2.Http()
+        h.request = mock_request
+        self.jwt.authorize(h)
+        h.request(self.url)
+
+        # Ensure we use the cached token
+        utcnow.return_value = T2_DATE
+        h.request(self.url)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    @mock.patch('time.time')
+    def test_authorize_no_aud(self, time, utcnow):
+        utcnow.return_value = T1_DATE
+        time.return_value = T1
+
+        jwt = service_account._JWTAccessCredentials(
+            self.service_account_email, self.signer,
+            private_key_id=self.private_key_id, client_id=self.client_id)
+
+        def mock_request(uri, method='GET', body=None, headers=None,
+                         redirections=0, connection_type=None):
+            self.assertEqual(uri, self.url)
+            bearer, token = headers[b'Authorization'].split()
+            payload = crypt.verify_signed_jwt_with_certs(
+                token,
+                {'key': datafile('public_cert.pem')},
+                audience=self.url)
+            self.assertEqual(payload['iss'], self.service_account_email)
+            self.assertEqual(payload['sub'], self.service_account_email)
+            self.assertEqual(payload['iat'], T1)
+            self.assertEqual(payload['exp'], T1_EXPIRY)
+            self.assertEqual(uri, self.url)
+            self.assertEqual(bearer, b'Bearer')
+            return httplib2.Response({'status': '200'}), b''
+
+        h = httplib2.Http()
+        h.request = mock_request
+        jwt.authorize(h)
+        h.request(self.url)
+
+        # Ensure we do not cache the token
+        self.assertIsNone(jwt.access_token)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_authorize_stale_token(self, utcnow):
+        utcnow.return_value = T1_DATE
+        # Create an initial token
+        h = HttpMockSequence([({'status': '200'}, b''),
+                              ({'status': '200'}, b'')])
+        self.jwt.authorize(h)
+        h.request(self.url)
+        token_1 = self.jwt.access_token
+
+        # Expire the token
+        utcnow.return_value = T3_DATE
+        h.request(self.url)
+        token_2 = self.jwt.access_token
+        self.assertEquals(self.jwt.token_expiry, T3_EXPIRY_DATE)
+        self.assertNotEqual(token_1, token_2)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_authorize_401(self, utcnow):
+        utcnow.return_value = T1_DATE
+
+        h = HttpMockSequence([
+            ({'status': '200'}, b''),
+            ({'status': '401'}, b''),
+            ({'status': '200'}, b'')])
+        self.jwt.authorize(h)
+        h.request(self.url)
+        token_1 = self.jwt.access_token
+
+        utcnow.return_value = T2_DATE
+        self.assertEquals(h.request(self.url)[0].status, 200)
+        token_2 = self.jwt.access_token
+        # Check the 401 forced a new token
+        self.assertNotEqual(token_1, token_2)
+
+    @mock.patch('oauth2client.client._UTCNOW')
+    def test_refresh(self, utcnow):
+        utcnow.return_value = T1_DATE
+        token_1 = self.jwt.access_token
+
+        utcnow.return_value = T2_DATE
+        self.jwt.refresh(None)
+        token_2 = self.jwt.access_token
+        self.assertEquals(self.jwt.token_expiry, T2_EXPIRY_DATE)
+        self.assertNotEqual(token_1, token_2)
diff --git a/tests/test_tools.py b/tests/test_tools.py
new file mode 100644
index 0000000..369f567
--- /dev/null
+++ b/tests/test_tools.py
@@ -0,0 +1,192 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import socket
+import sys
+import threading
+
+import mock
+from six.moves.urllib import request
+import unittest2
+
+from oauth2client import client
+from oauth2client import tools
+
+try:
+    import argparse
+except ImportError:  # pragma: NO COVER
+    raise unittest2.SkipTest('argparase unavailable.')
+
+
+class TestClientRedirectServer(unittest2.TestCase):
+    """Test the ClientRedirectServer and ClientRedirectHandler classes."""
+
+    def test_ClientRedirectServer(self):
+        # create a ClientRedirectServer and run it in a thread to listen
+        # for a mock GET request with the access token
+        # the server should return a 200 message and store the token
+        httpd = tools.ClientRedirectServer(('localhost', 0),
+                                           tools.ClientRedirectHandler)
+        code = 'foo'
+        url = 'http://localhost:{0}?code={1}'.format(
+            httpd.server_address[1], code)
+        t = threading.Thread(target=httpd.handle_request)
+        t.setDaemon(True)
+        t.start()
+        f = request.urlopen(url)
+        self.assertTrue(f.read())
+        t.join()
+        httpd.server_close()
+        self.assertEqual(httpd.query_params.get('code'), code)
+
+
+class TestRunFlow(unittest2.TestCase):
+
+    def setUp(self):
+        self.server = mock.Mock()
+        self.flow = mock.Mock()
+        self.storage = mock.Mock()
+        self.credentials = mock.Mock()
+
+        self.flow.step1_get_authorize_url.return_value = (
+            'http://example.com/auth')
+        self.flow.step2_exchange.return_value = self.credentials
+
+        self.flags = argparse.Namespace(
+            noauth_local_webserver=True, logging_level='INFO')
+        self.server_flags = argparse.Namespace(
+            noauth_local_webserver=False,
+            logging_level='INFO',
+            auth_host_port=[8080, ],
+            auth_host_name='localhost')
+
+    @mock.patch.object(sys, 'argv', ['ignored', '--noauth_local_webserver'])
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.input')
+    def test_run_flow_no_webserver(self, input_mock, logging_mock):
+        input_mock.return_value = 'auth_code'
+
+        # Successful exchange.
+        returned_credentials = tools.run_flow(self.flow, self.storage)
+
+        self.assertEqual(self.credentials, returned_credentials)
+        self.assertEqual(self.flow.redirect_uri, client.OOB_CALLBACK_URN)
+        self.flow.step2_exchange.assert_called_once_with(
+            'auth_code', http=None)
+        self.storage.put.assert_called_once_with(self.credentials)
+        self.credentials.set_store.assert_called_once_with(self.storage)
+
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.input')
+    def test_run_flow_no_webserver_explicit_flags(
+            self, input_mock, logging_mock):
+        input_mock.return_value = 'auth_code'
+
+        # Successful exchange.
+        returned_credentials = tools.run_flow(
+            self.flow, self.storage, flags=self.flags)
+
+        self.assertEqual(self.credentials, returned_credentials)
+        self.assertEqual(self.flow.redirect_uri, client.OOB_CALLBACK_URN)
+        self.flow.step2_exchange.assert_called_once_with(
+            'auth_code', http=None)
+
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.input')
+    def test_run_flow_no_webserver_exchange_error(
+            self, input_mock, logging_mock):
+        input_mock.return_value = 'auth_code'
+        self.flow.step2_exchange.side_effect = client.FlowExchangeError()
+
+        # Error while exchanging.
+        with self.assertRaises(SystemExit):
+            tools.run_flow(self.flow, self.storage, flags=self.flags)
+
+        self.flow.step2_exchange.assert_called_once_with(
+            'auth_code', http=None)
+
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.ClientRedirectServer')
+    @mock.patch('webbrowser.open')
+    def test_run_flow_webserver(
+            self, webbrowser_open_mock, server_ctor_mock, logging_mock):
+        server_ctor_mock.return_value = self.server
+        self.server.query_params = {'code': 'auth_code'}
+
+        # Successful exchange.
+        returned_credentials = tools.run_flow(
+            self.flow, self.storage, flags=self.server_flags)
+
+        self.assertEqual(self.credentials, returned_credentials)
+        self.assertEqual(self.flow.redirect_uri, 'http://localhost:8080/')
+        self.flow.step2_exchange.assert_called_once_with(
+            'auth_code', http=None)
+        self.storage.put.assert_called_once_with(self.credentials)
+        self.credentials.set_store.assert_called_once_with(self.storage)
+        self.assertTrue(self.server.handle_request.called)
+        webbrowser_open_mock.assert_called_once_with(
+            'http://example.com/auth', autoraise=True, new=1)
+
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.ClientRedirectServer')
+    @mock.patch('webbrowser.open')
+    def test_run_flow_webserver_exchange_error(
+            self, webbrowser_open_mock, server_ctor_mock, logging_mock):
+        server_ctor_mock.return_value = self.server
+        self.server.query_params = {'error': 'any error'}
+
+        # Exchange returned an error code.
+        with self.assertRaises(SystemExit):
+            tools.run_flow(self.flow, self.storage, flags=self.server_flags)
+
+        self.assertTrue(self.server.handle_request.called)
+
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.ClientRedirectServer')
+    @mock.patch('webbrowser.open')
+    def test_run_flow_webserver_no_code(
+            self, webbrowser_open_mock, server_ctor_mock, logging_mock):
+        server_ctor_mock.return_value = self.server
+        self.server.query_params = {}
+
+        # No code found in response
+        with self.assertRaises(SystemExit):
+            tools.run_flow(self.flow, self.storage, flags=self.server_flags)
+
+        self.assertTrue(self.server.handle_request.called)
+
+    @mock.patch('oauth2client.tools.logging')
+    @mock.patch('oauth2client.tools.ClientRedirectServer')
+    @mock.patch('oauth2client.tools.input')
+    def test_run_flow_webserver_fallback(
+            self, input_mock, server_ctor_mock, logging_mock):
+        server_ctor_mock.side_effect = socket.error()
+        input_mock.return_value = 'auth_code'
+
+        # It should catch the socket error and proceed as if
+        # noauth_local_webserver was specified.
+        returned_credentials = tools.run_flow(
+            self.flow, self.storage, flags=self.server_flags)
+
+        self.assertEqual(self.credentials, returned_credentials)
+        self.assertEqual(self.flow.redirect_uri, client.OOB_CALLBACK_URN)
+        self.flow.step2_exchange.assert_called_once_with(
+            'auth_code', http=None)
+        self.assertTrue(server_ctor_mock.called)
+        self.assertFalse(self.server.handle_request.called)
+
+
+class TestMessageIfMissing(unittest2.TestCase):
+    def test_message_if_missing(self):
+        self.assertIn('somefile.txt', tools.message_if_missing('somefile.txt'))
diff --git a/tests/test_transport.py b/tests/test_transport.py
new file mode 100644
index 0000000..e9782a8
--- /dev/null
+++ b/tests/test_transport.py
@@ -0,0 +1,131 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+import httplib2
+import mock
+import unittest2
+
+from oauth2client import client
+from oauth2client import transport
+
+
+class TestMemoryCache(unittest2.TestCase):
+
+    def test_get_set_delete(self):
+        cache = transport.MemoryCache()
+        self.assertIsNone(cache.get('foo'))
+        self.assertIsNone(cache.delete('foo'))
+        cache.set('foo', 'bar')
+        self.assertEqual('bar', cache.get('foo'))
+        cache.delete('foo')
+        self.assertIsNone(cache.get('foo'))
+
+
+class Test_get_cached_http(unittest2.TestCase):
+
+    def test_global(self):
+        cached_http = transport.get_cached_http()
+        self.assertIsInstance(cached_http, httplib2.Http)
+        self.assertIsInstance(cached_http.cache, transport.MemoryCache)
+
+    def test_value(self):
+        cache = object()
+        with mock.patch('oauth2client.transport._CACHED_HTTP', new=cache):
+            result = transport.get_cached_http()
+        self.assertIs(result, cache)
+
+
+class Test_get_http_object(unittest2.TestCase):
+
+    @mock.patch.object(httplib2, 'Http', return_value=object())
+    def test_it(self, http_klass):
+        result = transport.get_http_object()
+        self.assertEqual(result, http_klass.return_value)
+
+
+class Test__initialize_headers(unittest2.TestCase):
+
+    def test_null(self):
+        result = transport._initialize_headers(None)
+        self.assertEqual(result, {})
+
+    def test_copy(self):
+        headers = {'a': 1, 'b': 2}
+        result = transport._initialize_headers(headers)
+        self.assertEqual(result, headers)
+        self.assertIsNot(result, headers)
+
+
+class Test__apply_user_agent(unittest2.TestCase):
+
+    def test_null(self):
+        headers = object()
+        result = transport._apply_user_agent(headers, None)
+        self.assertIs(result, headers)
+
+    def test_new_agent(self):
+        headers = {}
+        user_agent = 'foo'
+        result = transport._apply_user_agent(headers, user_agent)
+        self.assertIs(result, headers)
+        self.assertEqual(result, {'user-agent': user_agent})
+
+    def test_append(self):
+        orig_agent = 'bar'
+        headers = {'user-agent': orig_agent}
+        user_agent = 'baz'
+        result = transport._apply_user_agent(headers, user_agent)
+        self.assertIs(result, headers)
+        final_agent = user_agent + ' ' + orig_agent
+        self.assertEqual(result, {'user-agent': final_agent})
+
+
+class Test_clean_headers(unittest2.TestCase):
+
+    def test_no_modify(self):
+        headers = {b'key': b'val'}
+        result = transport.clean_headers(headers)
+        self.assertIsNot(result, headers)
+        self.assertEqual(result, headers)
+
+    def test_cast_unicode(self):
+        headers = {u'key': u'val'}
+        header_bytes = {b'key': b'val'}
+        result = transport.clean_headers(headers)
+        self.assertIsNot(result, headers)
+        self.assertEqual(result, header_bytes)
+
+    def test_unicode_failure(self):
+        headers = {u'key': u'\u2603'}
+        with self.assertRaises(client.NonAsciiHeaderError):
+            transport.clean_headers(headers)
+
+    def test_cast_object(self):
+        headers = {b'key': True}
+        header_str = {b'key': b'True'}
+        result = transport.clean_headers(headers)
+        self.assertIsNot(result, headers)
+        self.assertEqual(result, header_str)
+
+
+class Test_wrap_http_for_auth(unittest2.TestCase):
+
+    def test_wrap(self):
+        credentials = object()
+        http = mock.Mock()
+        http.request = orig_req_method = object()
+        result = transport.wrap_http_for_auth(credentials, http)
+        self.assertIsNone(result)
+        self.assertNotEqual(http.request, orig_req_method)
+        self.assertIs(http.request.credentials, credentials)
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..533460f
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,122 @@
+"""Unit tests for oauth2client.util."""
+
+import mock
+import unittest2
+
+from oauth2client import util
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class PositionalTests(unittest2.TestCase):
+
+    def test_usage(self):
+        util.positional_parameters_enforcement = util.POSITIONAL_EXCEPTION
+
+        # 1 positional arg, 1 keyword-only arg.
+        @util.positional(1)
+        def fn(pos, kwonly=None):
+            return True
+
+        self.assertTrue(fn(1))
+        self.assertTrue(fn(1, kwonly=2))
+        with self.assertRaises(TypeError):
+            fn(1, 2)
+
+        # No positional, but a required keyword arg.
+        @util.positional(0)
+        def fn2(required_kw):
+            return True
+
+        self.assertTrue(fn2(required_kw=1))
+        with self.assertRaises(TypeError):
+            fn2(1)
+
+        # Unspecified positional, should automatically figure out 1 positional
+        # 1 keyword-only (same as first case above).
+        @util.positional
+        def fn3(pos, kwonly=None):
+            return True
+
+        self.assertTrue(fn3(1))
+        self.assertTrue(fn3(1, kwonly=2))
+        with self.assertRaises(TypeError):
+            fn3(1, 2)
+
+    @mock.patch('oauth2client.util.logger')
+    def test_enforcement_warning(self, mock_logger):
+        util.positional_parameters_enforcement = util.POSITIONAL_WARNING
+
+        @util.positional(1)
+        def fn(pos, kwonly=None):
+            return True
+
+        self.assertTrue(fn(1, 2))
+        self.assertTrue(mock_logger.warning.called)
+
+    @mock.patch('oauth2client.util.logger')
+    def test_enforcement_ignore(self, mock_logger):
+        util.positional_parameters_enforcement = util.POSITIONAL_IGNORE
+
+        @util.positional(1)
+        def fn(pos, kwonly=None):
+            return True
+
+        self.assertTrue(fn(1, 2))
+        self.assertFalse(mock_logger.warning.called)
+
+
+class ScopeToStringTests(unittest2.TestCase):
+
+    def test_iterables(self):
+        cases = [
+            ('', ''),
+            ('', ()),
+            ('', []),
+            ('', ('',)),
+            ('', ['', ]),
+            ('a', ('a',)),
+            ('b', ['b', ]),
+            ('a b', ['a', 'b']),
+            ('a b', ('a', 'b')),
+            ('a b', 'a b'),
+            ('a b', (s for s in ['a', 'b'])),
+        ]
+        for expected, case in cases:
+            self.assertEqual(expected, util.scopes_to_string(case))
+
+
+class StringToScopeTests(unittest2.TestCase):
+
+    def test_conversion(self):
+        cases = [
+            (['a', 'b'], ['a', 'b']),
+            ('', []),
+            ('a', ['a']),
+            ('a b c d e f', ['a', 'b', 'c', 'd', 'e', 'f']),
+        ]
+
+        for case, expected in cases:
+            self.assertEqual(expected, util.string_to_scopes(case))
+
+
+class AddQueryParameterTests(unittest2.TestCase):
+
+    def test__add_query_parameter(self):
+        self.assertEqual(
+            util._add_query_parameter('/action', 'a', None),
+            '/action')
+        self.assertEqual(
+            util._add_query_parameter('/action', 'a', 'b'),
+            '/action?a=b')
+        self.assertEqual(
+            util._add_query_parameter('/action?a=b', 'a', 'c'),
+            '/action?a=c')
+        # Order is non-deterministic.
+        self.assertIn(
+            util._add_query_parameter('/action?a=b', 'c', 'd'),
+            ['/action?a=b&c=d', '/action?c=d&a=b'])
+        self.assertEqual(
+            util._add_query_parameter('/action', 'a', ' ='),
+            '/action?a=+%3D')
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..b0781a8
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,178 @@
+[tox]
+envlist = py26,py27,py33,py34,py35,pypy,gae,cover
+
+[testenv]
+basedeps = mock>=1.3.0
+           pycrypto>=2.6
+           cryptography>=1.0
+           pyopenssl>=0.14
+           webtest
+           nose
+           flask
+           unittest2
+           sqlalchemy
+           fasteners
+deps = {[testenv]basedeps}
+       django
+       keyring
+setenv =
+    pypy: with_gmp=no
+    DJANGO_SETTINGS_MODULE=tests.contrib.django_util.settings
+commands = nosetests --ignore-files=test_appengine\.py --ignore-files=test__appengine_ndb\.py {posargs}
+
+[coverbase]
+basepython = python2.7
+commands =
+    nosetests \
+      --with-coverage \
+      --cover-package=oauth2client \
+      --cover-package=tests \
+      --cover-erase \
+      --cover-tests \
+      --cover-branches \
+      --ignore-files=test_appengine\.py \
+      --ignore-files=test__appengine_ndb\.py
+    nosetests \
+      --with-coverage \
+      --cover-package=oauth2client.contrib.appengine \
+      --cover-package=oauth2client.contrib._appengine_ndb \
+      --cover-package=tests.contrib.test_appengine \
+      --cover-package=tests.contrib.test__appengine_ndb \
+      --with-gae \
+      --cover-tests \
+      --cover-branches \
+      --gae-application=tests/data \
+      --gae-lib-root={env:GAE_PYTHONPATH:google_appengine} \
+      --logging-level=INFO \
+      tests/contrib/test_appengine.py \
+      tests/contrib/test__appengine_ndb.py
+deps = {[testenv]deps}
+    coverage
+    nosegae
+
+[testenv:py26]
+basepython =
+    python2.6
+commands =
+    nosetests \
+      --ignore-files=test_appengine\.py \
+      --ignore-files=test__appengine_ndb\.py \
+      --ignore-files=test_keyring_storage\.py \
+      --exclude-dir=oauth2client/contrib/django_util \
+      --exclude-dir=tests/contrib/django_util \
+      {posargs}
+deps = {[testenv]basedeps}
+       nose-exclude
+
+[testenv:py33]
+basepython =
+    python3.3
+commands =
+    nosetests \
+      --ignore-files=test_appengine\.py \
+      --ignore-files=test__appengine_ndb\.py \
+      --ignore-files=test_django_orm\.py \
+      --ignore-files=test_django_settings\.py \
+      --ignore-files=test_django_util\.py \
+      --exclude-dir=oauth2client/contrib/django_util \
+      --exclude-dir=tests/contrib/django_util \
+      {posargs}
+deps = {[testenv]basedeps}
+       keyring
+       nose-exclude
+
+[testenv:cover]
+basepython = {[coverbase]basepython}
+commands =
+    {[coverbase]commands}
+    coverage report --show-missing --cover-min-percentage=100
+deps =
+    {[coverbase]deps}
+
+[testenv:coveralls]
+basepython = {[coverbase]basepython}
+commands =
+    {[coverbase]commands}
+    coverage report --show-missing
+    coveralls
+deps =
+    {[coverbase]deps}
+    coveralls
+passenv = {[testenv:system-tests]passenv}
+
+[testenv:docs]
+basepython = python2.7
+deps =
+    {[testenv:cover]deps}
+    python-gflags
+    pyyaml
+    sphinx>=1.3b2
+    sphinx-rtd-theme
+    webapp2
+commands = {toxinidir}/scripts/build_docs.sh
+
+[testenv:gae]
+basepython = python2.7
+deps = {[testenv]basedeps}
+       nosegae
+commands =
+    nosetests \
+      --with-gae \
+      --gae-lib-root={env:GAE_PYTHONPATH:google_appengine} \
+      --gae-application=tests/data \
+      --logging-level=INFO \
+      tests/contrib/test_appengine.py \
+      tests/contrib/test__appengine_ndb.py
+
+[testenv:system-tests]
+basepython =
+    python2.7
+commands =
+    {toxinidir}/scripts/run_system_tests.sh
+deps =
+    pycrypto>=2.6
+    cryptography>=1.0
+    pyopenssl>=0.14
+passenv = GOOGLE_* OAUTH2CLIENT_* TRAVIS*
+
+[testenv:system-tests3]
+basepython =
+    python3.4
+commands =
+    {toxinidir}/scripts/run_system_tests.sh
+deps =
+    pycrypto>=2.6
+    cryptography>=1.0
+    pyopenssl>=0.14
+passenv = {[testenv:system-tests]passenv}
+
+[testenv:gce-system-tests]
+basepython =
+    python2.7
+commands =
+    python {toxinidir}/scripts/run_gce_system_tests.py
+deps =
+    pycrypto>=2.6
+    unittest2
+passenv = {[testenv:system-tests]passenv}
+
+[testenv:flake8]
+commands = flake8 --import-order-style google {posargs}
+deps =
+    flake8-putty
+    flake8-import-order
+
+[flake8]
+exclude = .tox,.git,./*.egg,build,
+application-import-names = oauth2client
+putty-ignore =
+  # E402 module level import not at top of file
+  # These files have needed configurations defined before import
+  docs/conf.py : E402
+  tests/contrib/test_appengine.py : E402
+  # Additionally, ignore E100 (imports in wrong order) for Django configuration
+  tests/contrib/test_django_orm.py : E402,I100
+  # E501 line too long
+  # Ignore lines over 80 chars that include "http:" or "https:"
+  /http:/ : E501
+  /https:/ : E501