ソースを参照

HUE-8978 [fb] Adding Google Cloud authentication dependencies

5 new libraries & updating version of pyasn1
https://github.com/pnpnpn/retry-decorator/tree/v1.0.0
https://github.com/googleapis/oauth2client/tree/v4.1.3
https://github.com/GoogleCloudPlatform/gcs-oauth2-boto-plugin/tree/v2.5
https://github.com/google/google-reauth-python/tree/0.1.0
https://github.com/google/pyu2f/tree/0.1.4

https://github.com/etingof/pyasn1/tree/v0.4.6
https://github.com/etingof/pyasn1-modules/tree/v0.2.6
Jean-Francois Desjeans Gauthier 6 年 前
コミット
ab639af720
100 ファイル変更11250 行追加0 行削除
  1. 1 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/.gitignore
  2. 21 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/CONTRIBUTING.md
  3. 203 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/LICENSE
  4. 49 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/README.md
  5. 31 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/__init__.py
  6. 788 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/oauth2_client.py
  7. 208 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/oauth2_helper.py
  8. 71 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/oauth2_plugin.py
  9. 381 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/test_oauth2_client.py
  10. 11 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/requirements.txt
  11. 85 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/setup.py
  12. 2 0
      desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/test-requirements.txt
  13. 12 0
      desktop/core/ext-py/google-reauth-python-0.1.0/.coveragerc
  14. 36 0
      desktop/core/ext-py/google-reauth-python-0.1.0/.gitignore
  15. 21 0
      desktop/core/ext-py/google-reauth-python-0.1.0/.travis.yml
  16. 7 0
      desktop/core/ext-py/google-reauth-python-0.1.0/CHANGELOG.rst
  17. 28 0
      desktop/core/ext-py/google-reauth-python-0.1.0/CONTRIBUTING.rst
  18. 201 0
      desktop/core/ext-py/google-reauth-python-0.1.0/LICENSE
  19. 3 0
      desktop/core/ext-py/google-reauth-python-0.1.0/MANIFEST.in
  20. 39 0
      desktop/core/ext-py/google-reauth-python-0.1.0/README.rst
  21. 0 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/__init__.py
  22. 44 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/_helpers.py
  23. 152 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/_reauth_client.py
  24. 132 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/challenges.py
  25. 62 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/errors.py
  26. 311 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/reauth.py
  27. 147 0
      desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/reauth_creds.py
  28. 42 0
      desktop/core/ext-py/google-reauth-python-0.1.0/nox.py
  29. 2 0
      desktop/core/ext-py/google-reauth-python-0.1.0/setup.cfg
  30. 63 0
      desktop/core/ext-py/google-reauth-python-0.1.0/setup.py
  31. 0 0
      desktop/core/ext-py/google-reauth-python-0.1.0/tests/__init__.py
  32. 80 0
      desktop/core/ext-py/google-reauth-python-0.1.0/tests/test_challenges.py
  33. 382 0
      desktop/core/ext-py/google-reauth-python-0.1.0/tests/test_reauth.py
  34. 239 0
      desktop/core/ext-py/google-reauth-python-0.1.0/tests/test_reauth_creds.py
  35. 16 0
      desktop/core/ext-py/oauth2client-4.1.3/.coveragerc
  36. 3 0
      desktop/core/ext-py/oauth2client-4.1.3/.github/ISSUE_TEMPLATE.md
  37. 3 0
      desktop/core/ext-py/oauth2client-4.1.3/.github/PULL_REQUEST_TEMPLATE.md
  38. 31 0
      desktop/core/ext-py/oauth2client-4.1.3/.gitignore
  39. 48 0
      desktop/core/ext-py/oauth2client-4.1.3/.travis.yml
  40. 504 0
      desktop/core/ext-py/oauth2client-4.1.3/CHANGELOG.md
  41. 43 0
      desktop/core/ext-py/oauth2client-4.1.3/CODE_OF_CONDUCT.md
  42. 202 0
      desktop/core/ext-py/oauth2client-4.1.3/CONTRIBUTING.md
  43. 95 0
      desktop/core/ext-py/oauth2client-4.1.3/CONTRIBUTORS.md
  44. 210 0
      desktop/core/ext-py/oauth2client-4.1.3/LICENSE
  45. 2 0
      desktop/core/ext-py/oauth2client-4.1.3/MANIFEST.in
  46. 5 0
      desktop/core/ext-py/oauth2client-4.1.3/Makefile
  47. 33 0
      desktop/core/ext-py/oauth2client-4.1.3/README.md
  48. 177 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/Makefile
  49. BIN
      desktop/core/ext-py/oauth2client-4.1.3/docs/_static/favicon.ico
  50. BIN
      desktop/core/ext-py/oauth2client-4.1.3/docs/_static/google_logo.png
  51. 114 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/conf.py
  52. 138 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/index.rst
  53. 10 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/requirements.txt
  54. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.client.rst
  55. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.clientsecrets.rst
  56. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.appengine.rst
  57. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.devshell.rst
  58. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.dictionary_storage.rst
  59. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.apps.rst
  60. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.decorators.rst
  61. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.models.rst
  62. 23 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.rst
  63. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.signals.rst
  64. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.site.rst
  65. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.storage.rst
  66. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.django_util.views.rst
  67. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.flask_util.rst
  68. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.gce.rst
  69. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.keyring_storage.rst
  70. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.multiprocess_file_storage.rst
  71. 32 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.rst
  72. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.sqlalchemy.rst
  73. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.xsrfutil.rst
  74. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.crypt.rst
  75. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.file.rst
  76. 30 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.rst
  77. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.service_account.rst
  78. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.tools.rst
  79. 7 0
      desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.transport.rst
  80. 24 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/__init__.py
  81. 341 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_helpers.py
  82. 136 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_openssl_crypt.py
  83. 67 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_pkce.py
  84. 184 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_pure_python_crypt.py
  85. 124 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_pycrypto_crypt.py
  86. 2170 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/client.py
  87. 173 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/clientsecrets.py
  88. 6 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/__init__.py
  89. 163 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/_appengine_ndb.py
  90. 118 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/_metadata.py
  91. 910 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/appengine.py
  92. 152 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/devshell.py
  93. 65 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/dictionary_storage.py
  94. 489 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/__init__.py
  95. 32 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/apps.py
  96. 145 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/decorators.py
  97. 82 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/models.py
  98. 28 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/signals.py
  99. 26 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/site.py
  100. 81 0
      desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/storage.py

+ 1 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/.gitignore

@@ -0,0 +1 @@
+.py[cod]

+ 21 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/CONTRIBUTING.md

@@ -0,0 +1,21 @@
+# How to become a contributor and submit your own code
+
+## Contributor License Agreements
+
+We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles.
+
+Please fill out either the individual or corporate Contributor License Agreement (CLA).
+
+* If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html).
+* If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html).
+
+Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests.
+
+## Contributing A Patch
+
+1. Submit an issue describing your proposed change to the repo in question.
+1. The repo owner will respond to your issue promptly.
+1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above).
+1. Fork the desired repo, develop and test your code changes.
+1. Submit a pull request.
+~                            

+ 203 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/LICENSE

@@ -0,0 +1,203 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+

+ 49 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/README.md

@@ -0,0 +1,49 @@
+# gcs-oauth2-boto-plugin
+
+gcs-oauth2-boto-plugin is a Python application whose purpose is to behave as
+an auth plugin for the [boto] auth plugin framework for use with [OAuth 2.0]
+credentials for the Google Cloud Platform. This plugin is compatible with
+both [user accounts] and [service accounts], and its functionality is
+essentially a wrapper around [oauth2client]
+with the addition of automatically caching tokens
+for the machine in a thread- and process-safe fashion.
+
+For more information about how to use this plugin to access Google Cloud Storage
+via boto in your application, see the [GCS documentation].
+
+If you wish to use this plugin without using the PyPI install as instructed in
+the documentation (e.g., for development), then you will need to manually
+acquire the modules from the requirements.txt file.
+
+
+When using this plugin, you must specify a client ID and secret. We offer the
+following methods for providing this information; if multiple methods are used,
+we will choose them in the following order:
+
+1. .boto config, if not set
+2. environment variables (OAUTH2_CLIENT_ID and OAUTH2_CLIENT_SECRET), if not set
+3. CLIENT_ID and CLIENT_SECRET values set by SetFallbackClientIdAndSecret function.
+
+Service accounts are supported via key files in either JSON or .p12 format.
+The file is first interpreted as JSON, with .p12 format as a fallback.
+
+The default locking mechanism used is threading.Lock. You can switch to using
+another locking mechanism by calling SetLock. Example:
+
+```
+SetLock(multiprocessing.Manager().Lock())
+```
+
+
+Before submitting any code, please run the tests (e.g., by creating a new
+virtualenv and running the following commands from the root of this repository):
+
+    pip install -r requirements.txt
+    PYTHONPATH="." python -m gcs_oauth2_boto_plugin.test_oauth2_client
+
+[boto]: https://github.com/boto/boto
+[OAuth 2.0]: https://developers.google.com/accounts/docs/OAuth2Login
+[user accounts]: https://developers.google.com/accounts/docs/OAuth2#installed
+[service accounts]: https://developers.google.com/accounts/docs/OAuth2#serviceaccount
+[oauth2client]: https://github.com/google/oauth2client
+[GCS documentation]: https://developers.google.com/storage/docs/gspythonlibrary

+ 31 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/__init__.py

@@ -0,0 +1,31 @@
+# Copyright 2014 Google Inc. All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+"""Plugin for Boto auth plugin framework, used with GCP OAuth2.0 credentials."""
+
+from __future__ import absolute_import
+
+# Import this module so that users can simply import the top-level module. Once
+# imported, boto will pick up our subclasses of AuthHandler.
+from gcs_oauth2_boto_plugin import oauth2_plugin
+
+from gcs_oauth2_boto_plugin.oauth2_helper import SetFallbackClientIdAndSecret
+from gcs_oauth2_boto_plugin.oauth2_helper import SetLock
+

+ 788 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/oauth2_client.py

@@ -0,0 +1,788 @@
+# 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 OAuth2 client library.
+
+This library provides a client implementation of the OAuth2 protocol (see
+https://developers.google.com/storage/docs/authentication.html#oauth).
+
+**** Experimental API ****
+
+This module is experimental and is subject to modification or removal without
+notice.
+"""
+
+# This implementation is a wrapper around the oauth2client implementation
+# that implements caching of access tokens independent of refresh
+# tokens (in the python API client oauth2client, there is a single class that
+# encapsulates both refresh and access tokens).
+
+from __future__ import absolute_import
+
+import datetime
+import errno
+from hashlib import sha1
+import json
+import logging
+import os
+import socket
+import tempfile
+import threading
+
+# pylint: disable=g-import-not-at-top
+if os.environ.get('USER_AGENT'):
+  import boto
+  boto.UserAgent += os.environ.get('USER_AGENT')
+
+# pylint: disable=g-bad-import-order
+import boto
+import httplib2
+import oauth2client.client
+import oauth2client.service_account
+from google_reauth import reauth_creds
+import retry_decorator.retry_decorator
+import socks
+
+import six
+from six import BytesIO
+from six.moves import urllib
+
+LOG = logging.getLogger('oauth2_client')
+
+# Lock used for checking/exchanging refresh token, so multithreaded
+# operation doesn't attempt concurrent refreshes.
+token_exchange_lock = threading.Lock()
+
+CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
+FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control'
+REAUTH_SCOPE = 'https://www.googleapis.com/auth/accounts.reauth'
+DEFAULT_SCOPE = FULL_CONTROL_SCOPE
+# Note that these scopes don't necessarily correspond to the refresh token
+# being used. This list is is used for obtaining the RAPT in the reauth
+# flow, to determine which challenges should be used. We define this at module
+# level to allow us to override it for other applications if needed.
+RAPT_SCOPES = [CLOUD_PLATFORM_SCOPE, REAUTH_SCOPE]
+
+METADATA_SERVER = 'http://metadata.google.internal'
+
+META_TOKEN_URI = (METADATA_SERVER + '/computeMetadata/v1/instance/'
+                  'service-accounts/default/token')
+
+META_HEADERS = {
+    'Metadata-Flavor': 'Google'
+}
+
+# pylint: disable=invalid-name
+_ServiceAccountCredentials = (
+    oauth2client.service_account.ServiceAccountCredentials)
+# pylint: enable=invalid-name
+
+
+# Note: this is copied from gsutil's gslib.cred_types. It should be kept in
+# sync. Also note that this library does not use HMAC, but it's preserved from
+# gsutil's copy to maintain compatibility.
+class CredTypes(object):
+  HMAC = 'HMAC'
+  OAUTH2_SERVICE_ACCOUNT = 'OAuth 2.0 Service Account'
+  OAUTH2_USER_ACCOUNT = 'Oauth 2.0 User Account'
+  GCE = 'GCE'
+
+
+class Error(Exception):
+  """Base exception for the OAuth2 module."""
+  pass
+
+
+class AuthorizationCodeExchangeError(Error):
+  """Error trying to exchange an authorization code into a refresh token."""
+  pass
+
+
+class TokenCache(object):
+  """Interface for OAuth2 token caches."""
+
+  def PutToken(self, key, value):
+    raise NotImplementedError
+
+  def GetToken(self, key):
+    raise NotImplementedError
+
+
+class NoopTokenCache(TokenCache):
+  """A stub implementation of TokenCache that does nothing."""
+
+  def PutToken(self, key, value):
+    pass
+
+  def GetToken(self, key):
+    return None
+
+
+class InMemoryTokenCache(TokenCache):
+  """An in-memory token cache.
+
+  The cache is implemented by a python dict, and inherits the thread-safety
+  properties of dict.
+  """
+
+  def __init__(self):
+    super(InMemoryTokenCache, self).__init__()
+    self.cache = dict()
+
+  def PutToken(self, key, value):
+    LOG.debug('InMemoryTokenCache.PutToken: key=%s', key)
+    self.cache[key] = value
+
+  def GetToken(self, key):
+    value = self.cache.get(key, None)
+    LOG.debug('InMemoryTokenCache.GetToken: key=%s%s present',
+              key, ' not' if value is None else '')
+    return value
+
+
+class FileSystemTokenCache(TokenCache):
+  """An implementation of a token cache that persists tokens on disk.
+
+  Each token object in the cache is stored in serialized form in a separate
+  file. The cache file's name can be configured via a path pattern that is
+  parameterized by the key under which a value is cached and optionally the
+  current processes uid as obtained by os.getuid().
+
+  Since file names are generally publicly visible in the system, it is important
+  that the cache key does not leak information about the token's value.  If
+  client code computes cache keys from token values, a cryptographically strong
+  one-way function must be used.
+  """
+
+  def __init__(self, path_pattern=None):
+    """Creates a FileSystemTokenCache.
+
+    Args:
+      path_pattern: Optional string argument to specify the path pattern for
+          cache files.  The argument should be a path with format placeholders
+          '%(key)s' and optionally '%(uid)s'.  If the argument is omitted, the
+          default pattern
+            <tmpdir>/oauth2client-tokencache.%(uid)s.%(key)s
+          is used, where <tmpdir> is replaced with the system temp dir as
+          obtained from tempfile.gettempdir().
+    """
+    super(FileSystemTokenCache, self).__init__()
+    self.path_pattern = path_pattern
+    if not path_pattern:
+      self.path_pattern = os.path.join(
+          tempfile.gettempdir(), 'oauth2_client-tokencache.%(uid)s.%(key)s')
+
+  def CacheFileName(self, key):
+    uid = '_'
+    try:
+      # os.getuid() doesn't seem to work in Windows
+      uid = str(os.getuid())
+    except:  # pylint: disable=bare-except
+      pass
+    return self.path_pattern % {'key': key, 'uid': uid}
+
+  def PutToken(self, key, value):
+    """Serializes the value to the key's filename.
+
+    To ensure that written tokens aren't leaked to a different users, we
+     a) unlink an existing cache file, if any (to ensure we don't fall victim
+        to symlink attacks and the like),
+     b) create a new file with O_CREAT | O_EXCL (to ensure nobody is trying to
+        race us)
+     If either of these steps fail, we simply give up (but log a warning). Not
+     caching access tokens is not catastrophic, and failure to create a file
+     can happen for either of the following reasons:
+      - someone is attacking us as above, in which case we want to default to
+        safe operation (not write the token);
+      - another legitimate process is racing us; in this case one of the two
+        will win and write the access token, which is fine;
+      - we don't have permission to remove the old file or write to the
+        specified directory, in which case we can't recover
+
+    Args:
+      key: the hash key to store.
+      value: the access_token value to serialize.
+    """
+
+    cache_file = self.CacheFileName(key)
+    LOG.debug('FileSystemTokenCache.PutToken: key=%s, cache_file=%s',
+              key, cache_file)
+    try:
+      os.unlink(cache_file)
+    except:  # pylint: disable=bare-except
+      # Ignore failure to unlink the file; if the file exists and can't be
+      # unlinked, the subsequent open with O_CREAT | O_EXCL will fail.
+      pass
+
+    flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
+
+    # Accommodate Windows; stolen from python2.6/tempfile.py.
+    if hasattr(os, 'O_NOINHERIT'):
+      flags |= os.O_NOINHERIT
+    if hasattr(os, 'O_BINARY'):
+      flags |= os.O_BINARY
+
+    try:
+      fd = os.open(cache_file, flags, 0o600)
+    except (OSError, IOError) as e:
+      LOG.warning('FileSystemTokenCache.PutToken: '
+                  'Failed to create cache file %s: %s', cache_file, e)
+      return
+    f = os.fdopen(fd, 'w+b')
+    serialized = value.Serialize()
+    if isinstance(serialized, six.text_type):
+      serialized = serialized.encode('utf-8')
+    f.write(six.ensure_binary(serialized))
+    f.close()
+
+  def GetToken(self, key):
+    """Returns a deserialized access token from the key's filename."""
+    value = None
+    cache_file = self.CacheFileName(key)
+
+    try:
+      f = open(cache_file)
+      value = AccessToken.UnSerialize(f.read())
+      f.close()
+    except (IOError, OSError) as e:
+      if e.errno != errno.ENOENT:
+        LOG.warning('FileSystemTokenCache.GetToken: '
+                    'Failed to read cache file %s: %s', cache_file, e)
+    except Exception as e:  # pylint: disable=broad-except
+      LOG.warning('FileSystemTokenCache.GetToken: '
+                  'Failed to read cache file %s (possibly corrupted): %s',
+                  cache_file, e)
+
+    LOG.debug('FileSystemTokenCache.GetToken: key=%s%s present (cache_file=%s)',
+              key, ' not' if value is None else '', cache_file)
+    return value
+
+
+class OAuth2Client(object):
+  """Common logic for OAuth2 clients."""
+
+  def __init__(self, cache_key_base, access_token_cache=None,
+               datetime_strategy=datetime.datetime, auth_uri=None,
+               token_uri=None, disable_ssl_certificate_validation=False,
+               proxy_host=None, proxy_port=None, proxy_user=None,
+               proxy_pass=None, ca_certs_file=None):
+    # datetime_strategy is used to invoke utcnow() on; it is injected into the
+    # constructor for unit testing purposes.
+    self.auth_uri = auth_uri
+    self.token_uri = token_uri
+    self.cache_key_base = cache_key_base
+    self.datetime_strategy = datetime_strategy
+    self.access_token_cache = access_token_cache or InMemoryTokenCache()
+    self.disable_ssl_certificate_validation = disable_ssl_certificate_validation
+    self.ca_certs_file = ca_certs_file
+    if proxy_host and proxy_port:
+      self._proxy_info = httplib2.ProxyInfo(socks.PROXY_TYPE_HTTP,
+                                            proxy_host,
+                                            proxy_port,
+                                            proxy_user=proxy_user,
+                                            proxy_pass=proxy_pass,
+                                            proxy_rdns=True)
+    else:
+      self._proxy_info = None
+
+  def CreateHttpRequest(self):
+    return httplib2.Http(
+        ca_certs=self.ca_certs_file,
+        disable_ssl_certificate_validation=(
+            self.disable_ssl_certificate_validation),
+        proxy_info=self._proxy_info)
+
+  def GetAccessToken(self):
+    """Obtains an access token for this client.
+
+    This client's access token cache is first checked for an existing,
+    not-yet-expired access token. If none is found, the client obtains a fresh
+    access token from the OAuth2 provider's token endpoint.
+
+    Returns:
+      The cached or freshly obtained AccessToken.
+    Raises:
+      oauth2client.client.AccessTokenRefreshError if an error occurs.
+    """
+    # Ensure only one thread at a time attempts to get (and possibly refresh)
+    # the access token. This doesn't prevent concurrent refresh attempts across
+    # multiple gsutil instances, but at least protects against multiple threads
+    # simultaneously attempting to refresh when gsutil -m is used.
+    token_exchange_lock.acquire()
+    try:
+      cache_key = self.CacheKey()
+      LOG.debug('GetAccessToken: checking cache for key %s', cache_key)
+      access_token = self.access_token_cache.GetToken(cache_key)
+      LOG.debug('GetAccessToken: token from cache: %s', access_token)
+      if access_token is None or access_token.ShouldRefresh():
+        rapt = None if access_token is None else access_token.rapt_token
+        LOG.debug('GetAccessToken: fetching fresh access token...')
+        access_token = self.FetchAccessToken(rapt_token=rapt)
+        LOG.debug('GetAccessToken: fresh access token: %s', access_token)
+        self.access_token_cache.PutToken(cache_key, access_token)
+      return access_token
+    finally:
+      token_exchange_lock.release()
+
+  def CacheKey(self):
+    """Computes a cache key.
+
+    The cache key is computed as the SHA1 hash of the refresh token for user
+    accounts, or the hash of the gs_service_client_id for service accounts,
+    which satisfies the FileSystemTokenCache requirement that cache keys do not
+    leak information about token values.
+
+    Returns:
+      A hash key.
+    """
+    h = sha1()
+    # Unicode objects (i.e. strings in Python 3) must be encoded before hashing
+    if isinstance(self.cache_key_base, six.text_type):
+      val = self.cache_key_base.encode('utf-8')
+    else:
+      val = self.cache_key_base
+    h.update(val)
+    return h.hexdigest()
+
+  def GetAuthorizationHeader(self):
+    """Gets the access token HTTP authorization header value.
+
+    Returns:
+      The value of an Authorization HTTP header that authenticates
+      requests with an OAuth2 access token.
+    """
+    return 'Bearer %s' % self.GetAccessToken().token
+
+
+class _BaseOAuth2ServiceAccountClient(OAuth2Client):
+  """Base class for OAuth2ServiceAccountClients.
+
+  Args:
+    client_id: The OAuth2 client ID of this client.
+    access_token_cache: An optional instance of a TokenCache. If omitted or
+        None, an InMemoryTokenCache is used.
+    auth_uri: The URI for OAuth2 authorization.
+    token_uri: The URI used to refresh access tokens.
+    datetime_strategy: datetime module strategy to use.
+    disable_ssl_certificate_validation: True if certifications should not be
+        validated.
+    proxy_host: An optional string specifying the host name of an HTTP proxy
+        to be used.
+    proxy_port: An optional int specifying the port number of an HTTP proxy
+        to be used.
+    proxy_user: An optional string specifying the user name for interacting
+        with the HTTP proxy.
+    proxy_pass: An optional string specifying the password for interacting
+        with the HTTP proxy.
+    ca_certs_file: The cacerts.txt file to use.
+  """
+
+  def __init__(self, client_id, access_token_cache=None, auth_uri=None,
+               token_uri=None, datetime_strategy=datetime.datetime,
+               disable_ssl_certificate_validation=False,
+               proxy_host=None, proxy_port=None, proxy_user=None,
+               proxy_pass=None, ca_certs_file=None):
+
+    super(_BaseOAuth2ServiceAccountClient, self).__init__(
+        cache_key_base=client_id, auth_uri=auth_uri, token_uri=token_uri,
+        access_token_cache=access_token_cache,
+        datetime_strategy=datetime_strategy,
+        disable_ssl_certificate_validation=disable_ssl_certificate_validation,
+        proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user,
+        proxy_pass=proxy_pass, ca_certs_file=ca_certs_file)
+    self._client_id = client_id
+
+  def FetchAccessToken(self, rapt_token=None):
+    credentials = self.GetCredentials()
+    http = self.CreateHttpRequest()
+    credentials.refresh(http)
+    return AccessToken(credentials.access_token, credentials.token_expiry,
+                       datetime_strategy=self.datetime_strategy,
+                       rapt_token=rapt_token)
+
+
+class OAuth2ServiceAccountClient(_BaseOAuth2ServiceAccountClient):
+  """An OAuth2 service account client using .p12 or .pem keys."""
+
+  def __init__(self, client_id, private_key, password,
+               access_token_cache=None, auth_uri=None, token_uri=None,
+               datetime_strategy=datetime.datetime,
+               disable_ssl_certificate_validation=False,
+               proxy_host=None, proxy_port=None, proxy_user=None,
+               proxy_pass=None, ca_certs_file=None):
+    # Avoid long repeated kwargs list.
+    # pylint: disable=g-doc-args
+    """Creates an OAuth2ServiceAccountClient.
+
+    Args:
+      client_id: The OAuth2 client ID of this client.
+      private_key: The private key associated with this service account.
+      password: The private key password used for the crypto signer.
+
+    Keyword arguments match the _BaseOAuth2ServiceAccountClient class.
+    """
+    # pylint: enable=g-doc-args
+    super(OAuth2ServiceAccountClient, self).__init__(
+        client_id,
+        auth_uri=auth_uri,
+        token_uri=token_uri,
+        access_token_cache=access_token_cache,
+        datetime_strategy=datetime_strategy,
+        disable_ssl_certificate_validation=disable_ssl_certificate_validation,
+        proxy_host=proxy_host,
+        proxy_port=proxy_port,
+        proxy_user=proxy_user,
+        proxy_pass=proxy_pass,
+        ca_certs_file=ca_certs_file)
+    self._private_key = private_key
+    self._password = password
+
+  def GetCredentials(self):
+    if oauth2client.client.HAS_CRYPTO:
+      # pylint: disable=protected-access
+      return _ServiceAccountCredentials.from_p12_keyfile_buffer(
+          self._client_id,
+          BytesIO(self._private_key),
+          private_key_password=self._password,
+          scopes=DEFAULT_SCOPE,
+          token_uri=self.token_uri)
+      # pylint: enable=protected-access
+    else:
+      raise MissingDependencyError(
+          'Service account authentication requires PyOpenSSL. Please install '
+          'this library and try again.')
+
+
+class OAuth2JsonServiceAccountClient(_BaseOAuth2ServiceAccountClient):
+  """An OAuth2 service account client using .json keys."""
+
+  def __init__(self, json_key_dict, access_token_cache=None, auth_uri=None,
+               token_uri=None, datetime_strategy=datetime.datetime,
+               disable_ssl_certificate_validation=False,
+               proxy_host=None, proxy_port=None, proxy_user=None,
+               proxy_pass=None, ca_certs_file=None):
+    # Avoid long repeated kwargs list.
+    # pylint: disable=g-doc-args
+    """Creates an OAuth2JsonServiceAccountClient.
+
+    Args:
+      json_key_dict: dictionary from the json private key file. Includes:
+          client_id: The OAuth2 client ID of this client.
+          client_email: The email associated with this client.
+          private_key_id: The private key id associated with this service
+              account.
+          private_key_pkcs8_text: The pkcs8 text containing the private key
+              data.
+
+    Keyword arguments match the _BaseOAuth2ServiceAccountClient class.
+    """
+    # pylint: enable=g-doc-args
+    super(OAuth2JsonServiceAccountClient, self).__init__(
+        json_key_dict['client_id'],
+        auth_uri=auth_uri,
+        token_uri=token_uri,
+        access_token_cache=access_token_cache,
+        datetime_strategy=datetime_strategy,
+        disable_ssl_certificate_validation=disable_ssl_certificate_validation,
+        proxy_host=proxy_host,
+        proxy_port=proxy_port,
+        proxy_user=proxy_user,
+        proxy_pass=proxy_pass,
+        ca_certs_file=ca_certs_file)
+    self._json_key_dict = json_key_dict
+    self._service_account_email = json_key_dict['client_email']
+    self._private_key_id = json_key_dict['private_key_id']
+    self._private_key_pkcs8_text = json_key_dict['private_key']
+
+  def GetCredentials(self):
+    return _ServiceAccountCredentials.from_json_keyfile_dict(
+        self._json_key_dict, scopes=[DEFAULT_SCOPE], token_uri=self.token_uri)
+
+
+class GsAccessTokenRefreshError(Exception):
+  """Transient error when requesting access token."""
+
+  def __init__(self, e):
+    super(GsAccessTokenRefreshError, self).__init__(e)
+
+
+class GsInvalidRefreshTokenError(Exception):
+
+  def __init__(self, e):
+    super(GsInvalidRefreshTokenError, self).__init__(e)
+
+
+class MissingDependencyError(Exception):
+
+  def __init__(self, e):
+    super(MissingDependencyError, self).__init__(e)
+
+
+class OAuth2UserAccountClient(OAuth2Client):
+  """An OAuth2 client."""
+
+  def __init__(self, token_uri, client_id, client_secret, refresh_token,
+               auth_uri=None, access_token_cache=None,
+               datetime_strategy=datetime.datetime,
+               disable_ssl_certificate_validation=False,
+               proxy_host=None, proxy_port=None, proxy_user=None,
+               proxy_pass=None, ca_certs_file=None):
+    """Creates an OAuth2UserAccountClient.
+
+    Args:
+      token_uri: The URI used to refresh access tokens.
+      client_id: The OAuth2 client ID of this client.
+      client_secret: The OAuth2 client secret of this client.
+      refresh_token: The token used to refresh the access token.
+      auth_uri: The URI for OAuth2 authorization.
+      access_token_cache: An optional instance of a TokenCache. If omitted or
+          None, an InMemoryTokenCache is used.
+      datetime_strategy: datetime module strategy to use.
+      disable_ssl_certificate_validation: True if certifications should not be
+          validated.
+      proxy_host: An optional string specifying the host name of an HTTP proxy
+          to be used.
+      proxy_port: An optional int specifying the port number of an HTTP proxy
+          to be used.
+      proxy_user: An optional string specifying the user name for interacting
+          with the HTTP proxy.
+      proxy_pass: An optional string specifying the password for interacting
+          with the HTTP proxy.
+      ca_certs_file: The cacerts.txt file to use.
+    """
+    super(OAuth2UserAccountClient, self).__init__(
+        cache_key_base=refresh_token,
+        auth_uri=auth_uri,
+        token_uri=token_uri,
+        access_token_cache=access_token_cache,
+        datetime_strategy=datetime_strategy,
+        disable_ssl_certificate_validation=disable_ssl_certificate_validation,
+        proxy_host=proxy_host,
+        proxy_port=proxy_port,
+        proxy_user=proxy_user,
+        proxy_pass=proxy_pass,
+        ca_certs_file=ca_certs_file)
+    self.token_uri = token_uri
+    self.client_id = client_id
+    self.client_secret = client_secret
+    self.refresh_token = refresh_token
+
+  def GetCredentials(self):
+    """Fetches a credentials objects from the provider's token endpoint."""
+    access_token = self.GetAccessToken()
+    credentials = reauth_creds.Oauth2WithReauthCredentials(
+        access_token.token,
+        self.client_id,
+        self.client_secret,
+        self.refresh_token,
+        access_token.expiry,
+        self.token_uri,
+        None)  # user_agent
+    return credentials
+
+  @retry_decorator.retry(
+      GsAccessTokenRefreshError,
+      tries=boto.config.get('OAuth2', 'oauth2_refresh_retries', 6),
+      timeout_secs=1)
+  def FetchAccessToken(self, rapt_token=None):
+    """Fetches an access token from the provider's token endpoint.
+
+    Fetches an access token from this client's OAuth2 provider's token endpoint.
+
+    Args:
+      rapt_token: (str) The RAPT to be passed when refreshing the access token.
+
+    Returns:
+      The fetched AccessToken.
+    """
+    try:
+      http = self.CreateHttpRequest()
+      credentials = reauth_creds.Oauth2WithReauthCredentials(
+          None,  # access_token
+          self.client_id,
+          self.client_secret,
+          self.refresh_token,
+          None,  # token_expiry
+          self.token_uri,
+          None,  # user_agent
+          scopes=RAPT_SCOPES,
+          rapt_token=rapt_token)
+      credentials.refresh(http)
+      return AccessToken(
+          credentials.access_token,
+          credentials.token_expiry,
+          datetime_strategy=self.datetime_strategy,
+          rapt_token=credentials.rapt_token)
+    except oauth2client.client.AccessTokenRefreshError as e:
+      if 'Invalid response 403' in e.message:
+        # This is the most we can do at the moment to accurately detect rate
+        # limiting errors since they come back as 403s with no further
+        # information.
+        raise GsAccessTokenRefreshError(e)
+      elif 'invalid_grant' in e.message:
+        LOG.info("""
+Attempted to retrieve an access token from an invalid refresh token. Two common
+cases in which you will see this error are:
+1. Your refresh token was revoked.
+2. Your refresh token was typed incorrectly.
+""")
+        raise GsInvalidRefreshTokenError(e)
+      else:
+        raise
+
+
+class OAuth2GCEClient(OAuth2Client):
+  """OAuth2 client for GCE instance."""
+
+  def __init__(self):
+    super(OAuth2GCEClient, self).__init__(
+        cache_key_base='',
+        # Only InMemoryTokenCache can be used with empty cache_key_base.
+        access_token_cache=InMemoryTokenCache())
+
+  @retry_decorator.retry(GsAccessTokenRefreshError, tries=6, timeout_secs=1)
+  def FetchAccessToken(self, rapt_token=None):
+    """Fetches an access token from the provider's token endpoint.
+
+    Fetches an access token from the GCE metadata server.
+
+    Args:
+      rapt_token: (str) Ignored for this class. Service accounts don't use
+          reauth credentials.
+
+    Returns:
+      The fetched AccessToken.
+    """
+
+    del rapt_token  # Unused for service account credentials.
+    # Note that rapt_token is used only for user credentials, and thus should
+    # always be None in the context of GCE (service account) credentials.
+    response = None
+    try:
+      http = httplib2.Http()
+      response, content = http.request(META_TOKEN_URI, method='GET',
+                                       body=None, headers=META_HEADERS)
+      content = six.ensure_text(content)
+    except Exception as e:
+      raise GsAccessTokenRefreshError(e)
+
+    if response.status == 200:
+      d = json.loads(content)
+
+      return AccessToken(
+          d['access_token'],
+          datetime.datetime.now() +
+          datetime.timedelta(seconds=d.get('expires_in', 0)),
+          datetime_strategy=self.datetime_strategy,
+          rapt_token=None)
+
+
+def _IsGCE():
+  """Returns True if running on a GCE instance, otherwise False."""
+  try:
+    http = httplib2.Http()
+    response, _ = http.request(METADATA_SERVER)
+    return response.status == 200
+
+  except (httplib2.ServerNotFoundError, socket.error):
+    # We might see something like "No route to host" propagated as a socket
+    # error. We might also catch transient socket errors, but at that point
+    # we're going to fail anyway, just with a different error message. With
+    # this approach, we'll avoid having to enumerate all possible non-transient
+    # socket errors.
+    return False
+  except Exception as e:  # pylint: disable=broad-except
+    LOG.warning("Failed to determine whether we're running on GCE, so we'll"
+                "assume that we aren't: %s", e)
+    return False
+
+  return False
+
+
+def CreateOAuth2GCEClient():
+  return OAuth2GCEClient() if _IsGCE() else None
+
+
+class AccessToken(object):
+  """Encapsulates an OAuth2 access token."""
+
+  def __init__(self, token, expiry, datetime_strategy=datetime.datetime,
+               rapt_token=None):
+    self.token = token
+    self.expiry = expiry
+    self.datetime_strategy = datetime_strategy
+    # RAPT isn't technically part of the access token, but we can use the
+    # RAPT from an expired access token when refreshing to get a new access
+    # token. This allows users to only answer reauth challenges when the RAPT
+    # expires (usually ~once a day), as opposed to every time the access token
+    # expires (usually after an hour).
+    self.rapt_token = rapt_token
+
+  @staticmethod
+  def UnSerialize(query):
+    """Creates an AccessToken object from its serialized form."""
+
+    def GetValue(d, key):
+      return (d.get(key, [None]))[0]
+    kv = urllib.parse.parse_qs(query)
+    if 'token' not in kv or not kv['token']:
+      return None
+    expiry = None
+    expiry_tuple = GetValue(kv, 'expiry')
+    if expiry_tuple:
+      try:
+        expiry = datetime.datetime(
+            *[int(n) for n in expiry_tuple.split(',')])
+      except:  # pylint: disable=bare-except
+        return None
+    rapt_token = GetValue(kv, 'rapt_token')
+    return AccessToken(GetValue(kv, 'token'), expiry, rapt_token=rapt_token)
+
+  def Serialize(self):
+    """Serializes this object as URI-encoded key-value pairs."""
+    # There's got to be a better way to serialize a datetime. Unfortunately,
+    # there is no reliable way to convert into a unix epoch.
+    kv = {'token': self.token}
+    if self.expiry:
+      t = self.expiry
+      tupl = (t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond)
+      kv['expiry'] = ','.join([str(i) for i in tupl])
+    if self.rapt_token:
+      kv['rapt_token'] = self.rapt_token
+    return urllib.parse.urlencode(kv)
+
+  def ShouldRefresh(self, time_delta=300):
+    """Whether the access token needs to be refreshed.
+
+    Args:
+      time_delta: refresh access token when it expires within time_delta secs.
+
+    Returns:
+      True if the token is expired or about to expire, False if the
+      token should be expected to work.  Note that the token may still
+      be rejected, e.g. if it has been revoked server-side.
+    """
+    if self.expiry is None:
+      return False
+    return (self.datetime_strategy.utcnow()
+            + datetime.timedelta(seconds=time_delta) > self.expiry)
+
+  def __eq__(self, other):
+    return self.token == other.token and self.expiry == other.expiry
+
+  def __ne__(self, other):
+    return not self.__eq__(other)
+
+  def __str__(self):
+    return 'AccessToken(token=%s, expiry=%sZ)' % (self.token, self.expiry)

+ 208 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/oauth2_helper.py

@@ -0,0 +1,208 @@
+# 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.
+
+"""Helper routines to facilitate use of oauth2_client."""
+
+from __future__ import absolute_import
+
+import io
+import json
+import os
+import sys
+import time
+import webbrowser
+
+from gcs_oauth2_boto_plugin import oauth2_client
+import oauth2client.client
+
+from six.moves import input  # pylint: disable=redefined-builtin
+
+UTF8 = 'utf-8'
+CLIENT_ID = None
+CLIENT_SECRET = None
+
+GOOGLE_OAUTH2_PROVIDER_AUTHORIZATION_URI = (
+    'https://accounts.google.com/o/oauth2/auth')
+GOOGLE_OAUTH2_PROVIDER_TOKEN_URI = (
+    'https://oauth2.googleapis.com/token')
+GOOGLE_OAUTH2_DEFAULT_FILE_PASSWORD = 'notasecret'
+
+OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
+
+
+def OAuth2ClientFromBotoConfig(
+    config, cred_type=oauth2_client.CredTypes.OAUTH2_USER_ACCOUNT):
+  """Create a client type based on credentials supplied in boto config."""
+  token_cache = None
+  token_cache_type = config.get('OAuth2', 'token_cache', 'file_system')
+  if token_cache_type == 'file_system':
+    if config.has_option('OAuth2', 'token_cache_path_pattern'):
+      token_cache = oauth2_client.FileSystemTokenCache(
+          path_pattern=config.get('OAuth2', 'token_cache_path_pattern'))
+    else:
+      token_cache = oauth2_client.FileSystemTokenCache()
+  elif token_cache_type == 'in_memory':
+    token_cache = oauth2_client.InMemoryTokenCache()
+  else:
+    raise Exception(
+        'Invalid value for config option OAuth2/token_cache: %s' %
+        token_cache_type)
+
+  proxy_host = None
+  proxy_port = None
+  proxy_user = None
+  proxy_pass = None
+  if (config.has_option('Boto', 'proxy')
+      and config.has_option('Boto', 'proxy_port')):
+    proxy_host = config.get('Boto', 'proxy')
+    proxy_port = int(config.get('Boto', 'proxy_port'))
+    proxy_user = config.get('Boto', 'proxy_user', None)
+    proxy_pass = config.get('Boto', 'proxy_pass', None)
+
+  provider_authorization_uri = config.get(
+      'OAuth2', 'provider_authorization_uri',
+      GOOGLE_OAUTH2_PROVIDER_AUTHORIZATION_URI)
+  provider_token_uri = config.get(
+      'OAuth2', 'provider_token_uri', GOOGLE_OAUTH2_PROVIDER_TOKEN_URI)
+
+  if cred_type == oauth2_client.CredTypes.OAUTH2_SERVICE_ACCOUNT:
+    service_client_id = config.get('Credentials', 'gs_service_client_id', '')
+    private_key_filename = config.get('Credentials', 'gs_service_key_file', '')
+    with io.open(private_key_filename, 'rb') as private_key_file:
+      private_key = private_key_file.read()
+
+    keyfile_is_utf8 = False
+    try:
+      private_key = private_key.decode(UTF8)
+      # P12 keys won't be encoded as UTF8 bytes.
+      keyfile_is_utf8 = True
+    except UnicodeDecodeError:
+      pass
+
+    if keyfile_is_utf8:
+      try:
+        json_key_dict = json.loads(private_key)
+      except ValueError:
+        raise Exception('Could not parse JSON keyfile "%s" as valid JSON' %
+                        private_key_filename)
+      for json_entry in ('client_id', 'client_email', 'private_key_id',
+                         'private_key'):
+        if json_entry not in json_key_dict:
+          raise Exception('The JSON private key file at %s '
+                          'did not contain the required entry: %s' %
+                          (private_key_filename, json_entry))
+      return oauth2_client.OAuth2JsonServiceAccountClient(
+          json_key_dict, access_token_cache=token_cache,
+          auth_uri=provider_authorization_uri, token_uri=provider_token_uri,
+          disable_ssl_certificate_validation=not(config.getbool(
+              'Boto', 'https_validate_certificates', True)),
+          proxy_host=proxy_host, proxy_port=proxy_port,
+          proxy_user=proxy_user, proxy_pass=proxy_pass)
+    else:
+      key_file_pass = config.get('Credentials', 'gs_service_key_file_password',
+                                 GOOGLE_OAUTH2_DEFAULT_FILE_PASSWORD)
+
+      return oauth2_client.OAuth2ServiceAccountClient(
+          service_client_id, private_key, key_file_pass,
+          access_token_cache=token_cache, auth_uri=provider_authorization_uri,
+          token_uri=provider_token_uri,
+          disable_ssl_certificate_validation=not(config.getbool(
+              'Boto', 'https_validate_certificates', True)),
+          proxy_host=proxy_host, proxy_port=proxy_port,
+          proxy_user=proxy_user, proxy_pass=proxy_pass)
+
+  elif cred_type == oauth2_client.CredTypes.OAUTH2_USER_ACCOUNT:
+    client_id = config.get('OAuth2', 'client_id',
+                           os.environ.get('OAUTH2_CLIENT_ID', CLIENT_ID))
+    if not client_id:
+      raise Exception(
+          'client_id for your application obtained from '
+          'https://console.developers.google.com must be set in a boto config '
+          'or with OAUTH2_CLIENT_ID environment variable or with '
+          'gcs_oauth2_boto_plugin.SetFallbackClientIdAndSecret function.')
+
+    client_secret = config.get('OAuth2', 'client_secret',
+                               os.environ.get('OAUTH2_CLIENT_SECRET',
+                                              CLIENT_SECRET))
+    ca_certs_file = config.get_value('Boto', 'ca_certificates_file')
+    if ca_certs_file == 'system':
+      ca_certs_file = None
+
+    if not client_secret:
+      raise Exception(
+          'client_secret for your application obtained from '
+          'https://console.developers.google.com must be set in a boto config '
+          'or with OAUTH2_CLIENT_SECRET environment variable or with '
+          'gcs_oauth2_boto_plugin.SetFallbackClientIdAndSecret function.')
+    return oauth2_client.OAuth2UserAccountClient(
+        provider_token_uri, client_id, client_secret,
+        config.get('Credentials', 'gs_oauth2_refresh_token'),
+        auth_uri=provider_authorization_uri, access_token_cache=token_cache,
+        disable_ssl_certificate_validation=not(config.getbool(
+            'Boto', 'https_validate_certificates', True)),
+        proxy_host=proxy_host, proxy_port=proxy_port,
+        proxy_user=proxy_user, proxy_pass=proxy_pass,
+        ca_certs_file=ca_certs_file)
+  else:
+    raise Exception('You have attempted to create an OAuth2 client without '
+                    'setting up OAuth2 credentials.')
+
+
+def OAuth2ApprovalFlow(client, scopes, launch_browser=False):
+  """Run the OAuth2 flow to fetch a refresh token. Returns the refresh token."""
+  flow = oauth2client.client.OAuth2WebServerFlow(
+      client.client_id, client.client_secret, scopes, auth_uri=client.auth_uri,
+      token_uri=client.token_uri, redirect_uri=OOB_REDIRECT_URI)
+  approval_url = flow.step1_get_authorize_url()
+
+  if launch_browser:
+    sys.stdout.write(
+        'Attempting to launch a browser with the OAuth2 approval dialog at '
+        'URL: %s\n\n'
+        '[Note: due to a Python bug, you may see a spurious error message '
+        '"object is not\ncallable [...] in [...] Popen.__del__" which can be '
+        'ignored.]\n\n' % approval_url)
+  else:
+    sys.stdout.write(
+        'Please navigate your browser to the following URL:\n%s\n' %
+        approval_url)
+
+  sys.stdout.write(
+      'In your browser you should see a page that requests you to authorize '
+      'access to Google Cloud Platform APIs and Services on your behalf. '
+      'After you approve, an authorization code will be displayed.\n\n')
+  if (launch_browser and
+      not webbrowser.open(approval_url, new=1, autoraise=True)):
+    sys.stdout.write(
+        'Launching browser appears to have failed; please navigate a browser '
+        'to the following URL:\n%s\n' % approval_url)
+  # Short delay; webbrowser.open on linux insists on printing out a message
+  # which we don't want to run into the prompt for the auth code.
+  time.sleep(2)
+  code = input('Enter the authorization code: ')
+  credentials = flow.step2_exchange(code, http=client.CreateHttpRequest())
+  return credentials.refresh_token
+
+
+def SetFallbackClientIdAndSecret(client_id, client_secret):
+  global CLIENT_ID
+  global CLIENT_SECRET
+
+  CLIENT_ID = client_id
+  CLIENT_SECRET = client_secret
+
+
+def SetLock(lock):
+  oauth2_client.token_exchange_lock = lock
+

+ 71 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/oauth2_plugin.py

@@ -0,0 +1,71 @@
+# 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.
+
+"""Boto auth plugin for OAuth2.0 for Google Cloud Storage."""
+
+from __future__ import absolute_import
+
+import boto.auth_handler
+
+from gcs_oauth2_boto_plugin import oauth2_client
+from gcs_oauth2_boto_plugin import oauth2_helper
+
+IS_SERVICE_ACCOUNT = False
+
+
+class OAuth2Auth(boto.auth_handler.AuthHandler):
+  """AuthHandler for working with OAuth2 user account credentials."""
+
+  capability = ['google-oauth2', 's3']
+
+  def __init__(self, path, config, provider):
+    self.oauth2_client = None
+    if provider.name == 'google':
+      if config.has_option('Credentials', 'gs_oauth2_refresh_token'):
+        self.oauth2_client = oauth2_helper.OAuth2ClientFromBotoConfig(config)
+      elif config.has_option('GoogleCompute', 'service_account'):
+        self.oauth2_client = oauth2_client.CreateOAuth2GCEClient()
+    if not self.oauth2_client:
+      raise boto.auth_handler.NotReadyToAuthenticate()
+
+  def add_auth(self, http_request):
+    http_request.headers['Authorization'] = (
+        self.oauth2_client.GetAuthorizationHeader())
+
+
+class OAuth2ServiceAccountAuth(boto.auth_handler.AuthHandler):
+  """AuthHandler for working with OAuth2 service account credentials."""
+
+  capability = ['google-oauth2', 's3']
+
+  def __init__(self, path, config, provider):
+    if (provider.name == 'google'
+        and config.has_option('Credentials', 'gs_service_key_file')):
+      self.oauth2_client = oauth2_helper.OAuth2ClientFromBotoConfig(
+          config, cred_type=oauth2_client.CredTypes.OAUTH2_SERVICE_ACCOUNT)
+
+      # If we make it to this point, then we will later attempt to authenticate
+      # as a service account based on how the boto auth plugins work. This is
+      # global so that command.py can access this value once it's set.
+      # TODO: replace this approach with a way to get the current plugin
+      # from boto so that we don't have to have global variables.
+      global IS_SERVICE_ACCOUNT
+      IS_SERVICE_ACCOUNT = True
+    else:
+      raise boto.auth_handler.NotReadyToAuthenticate()
+
+  def add_auth(self, http_request):
+    http_request.headers['Authorization'] = (
+        self.oauth2_client.GetAuthorizationHeader())
+

+ 381 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/gcs_oauth2_boto_plugin/test_oauth2_client.py

@@ -0,0 +1,381 @@
+# 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 oauth2_client and related classes."""
+
+from __future__ import absolute_import
+
+import datetime
+import logging
+import os
+import stat
+import sys
+import unittest
+
+from freezegun import freeze_time
+from gcs_oauth2_boto_plugin import oauth2_client
+import httplib2
+import mock
+
+LOG = logging.getLogger('test_oauth2_client')
+
+ACCESS_TOKEN = 'abc123'
+RAPT_TOKEN = 'rapt123'
+TOKEN_URI = 'https://provider.example.com/oauth/provider?mode=token'
+AUTH_URI = 'https://provider.example.com/oauth/provider?mode=authorize'
+DEFAULT_CA_CERTS_FILE = os.path.abspath(
+    os.path.join('gslib', 'data', 'cacerts.txt'))
+
+IS_WINDOWS = 'win32' in str(sys.platform).lower()
+
+
+class MockDateTime(object):
+
+  def __init__(self):
+    self.mock_now = None
+
+  def utcnow(self):  # pylint: disable=invalid-name
+    return self.mock_now
+
+
+class MockOAuth2ServiceAccountClient(oauth2_client.OAuth2ServiceAccountClient):
+  """Mock service account client for testing OAuth2 with service accounts."""
+
+  def __init__(self, client_id, private_key, password, auth_uri, token_uri,
+               datetime_strategy):
+    super(MockOAuth2ServiceAccountClient, self).__init__(
+        client_id, private_key, password, auth_uri=auth_uri,
+        token_uri=token_uri, datetime_strategy=datetime_strategy,
+        ca_certs_file=DEFAULT_CA_CERTS_FILE)
+    self.Reset()
+
+  def Reset(self):
+    self.fetched_token = False
+
+  def FetchAccessToken(self, rapt_token=None):
+    self.fetched_token = True
+    return oauth2_client.AccessToken(
+        ACCESS_TOKEN,
+        GetExpiry(self.datetime_strategy, 3600),
+        datetime_strategy=self.datetime_strategy,
+        rapt_token=None)
+
+
+class MockOAuth2UserAccountClient(oauth2_client.OAuth2UserAccountClient):
+  """Mock user account client for testing OAuth2 with user accounts."""
+
+  def __init__(self, token_uri, client_id, client_secret, refresh_token,
+               auth_uri, datetime_strategy):
+    super(MockOAuth2UserAccountClient, self).__init__(
+        token_uri, client_id, client_secret, refresh_token, auth_uri=auth_uri,
+        datetime_strategy=datetime_strategy,
+        ca_certs_file=DEFAULT_CA_CERTS_FILE)
+    self.Reset()
+
+  def Reset(self):
+    self.fetched_token = False
+
+  def FetchAccessToken(self, rapt_token=None):
+    self.fetched_token = True
+    return oauth2_client.AccessToken(
+        ACCESS_TOKEN,
+        GetExpiry(self.datetime_strategy, 3600),
+        datetime_strategy=self.datetime_strategy,
+        rapt_token=RAPT_TOKEN if rapt_token is None else rapt_token)
+
+
+def GetExpiry(datetime_strategy, length_in_seconds):
+  token_expiry = (datetime_strategy.utcnow()
+                  + datetime.timedelta(seconds=length_in_seconds))
+  return token_expiry
+
+
+def CreateMockUserAccountClient(mock_datetime):
+  return MockOAuth2UserAccountClient(
+      TOKEN_URI, 'clid', 'clsecret', 'ref_token_abc123', AUTH_URI,
+      mock_datetime)
+
+
+def CreateMockServiceAccountClient(mock_datetime):
+  return MockOAuth2ServiceAccountClient(
+      'clid', 'private_key', 'password', AUTH_URI, TOKEN_URI, mock_datetime)
+
+
+class OAuth2AccountClientTest(unittest.TestCase):
+  """Unit tests for OAuth2UserAccountClient and OAuth2ServiceAccountClient."""
+
+  def setUp(self):
+    self.tempdirs = []
+    self.mock_datetime = MockDateTime()
+    self.start_time = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
+    self.mock_datetime.mock_now = self.start_time
+
+  def testGetAccessTokenUserAccount(self):
+    self.client = CreateMockUserAccountClient(self.mock_datetime)
+    self._RunGetAccessTokenTest(expected_rapt=RAPT_TOKEN)
+
+  def testGetAccessTokenServiceAccount(self):
+    self.client = CreateMockServiceAccountClient(self.mock_datetime)
+    self._RunGetAccessTokenTest()
+
+  def _RunGetAccessTokenTest(self, expected_rapt=None):
+    """Tests access token gets with self.client."""
+    access_token_1 = 'abc123'
+
+    self.assertFalse(self.client.fetched_token)
+    token_1 = self.client.GetAccessToken()
+
+    # There's no access token in the cache; verify that we fetched a fresh
+    # token.
+    self.assertTrue(self.client.fetched_token)
+    self.assertEqual(access_token_1, token_1.token)
+    self.assertEqual(self.start_time + datetime.timedelta(minutes=60),
+                     token_1.expiry)
+    self.assertEqual(token_1.rapt_token, expected_rapt)
+
+    # Advance time by less than expiry time, and fetch another token.
+    self.client.Reset()
+    self.mock_datetime.mock_now = (
+        self.start_time + datetime.timedelta(minutes=55))
+    token_2 = self.client.GetAccessToken()
+
+    # Since the access token wasn't expired, we get the cache token, and there
+    # was no refresh request.
+    self.assertEqual(token_1, token_2)
+    self.assertEqual(access_token_1, token_2.token)
+    self.assertFalse(self.client.fetched_token)
+
+    # Advance time past expiry time, and fetch another token.
+    self.client.Reset()
+    self.mock_datetime.mock_now = (
+        self.start_time + datetime.timedelta(minutes=55, seconds=1))
+    self.client.datetime_strategy = self.mock_datetime
+    token_3 = self.client.GetAccessToken()
+
+    # This should have resulted in a refresh request and a fresh access token.
+    self.assertTrue(self.client.fetched_token)
+    self.assertEqual(
+        self.mock_datetime.mock_now + datetime.timedelta(minutes=60),
+        token_3.expiry)
+    self.assertEqual(token_3.rapt_token, expected_rapt)
+
+
+class AccessTokenTest(unittest.TestCase):
+  """Unit tests for access token functions."""
+
+  def testShouldRefresh(self):
+    """Tests that token.ShouldRefresh returns the correct value."""
+    mock_datetime = MockDateTime()
+    start = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
+    expiry = start + datetime.timedelta(minutes=60)
+    token = oauth2_client.AccessToken(
+        'foo', expiry, datetime_strategy=mock_datetime)
+
+    mock_datetime.mock_now = start
+    self.assertFalse(token.ShouldRefresh())
+
+    mock_datetime.mock_now = start + datetime.timedelta(minutes=54)
+    self.assertFalse(token.ShouldRefresh())
+
+    mock_datetime.mock_now = start + datetime.timedelta(minutes=55)
+    self.assertFalse(token.ShouldRefresh())
+
+    mock_datetime.mock_now = start + datetime.timedelta(
+        minutes=55, seconds=1)
+    self.assertTrue(token.ShouldRefresh())
+
+    mock_datetime.mock_now = start + datetime.timedelta(
+        minutes=61)
+    self.assertTrue(token.ShouldRefresh())
+
+    mock_datetime.mock_now = start + datetime.timedelta(minutes=58)
+    self.assertFalse(token.ShouldRefresh(time_delta=120))
+
+    mock_datetime.mock_now = start + datetime.timedelta(
+        minutes=58, seconds=1)
+    self.assertTrue(token.ShouldRefresh(time_delta=120))
+
+  def testShouldRefreshNoExpiry(self):
+    """Tests token.ShouldRefresh with no expiry time."""
+    mock_datetime = MockDateTime()
+    start = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
+    token = oauth2_client.AccessToken(
+        'foo', None, datetime_strategy=mock_datetime)
+
+    mock_datetime.mock_now = start
+    self.assertFalse(token.ShouldRefresh())
+
+    mock_datetime.mock_now = start + datetime.timedelta(
+        minutes=472)
+    self.assertFalse(token.ShouldRefresh())
+
+  def testSerialization(self):
+    """Tests token serialization."""
+    expiry = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
+    token = oauth2_client.AccessToken('foo', expiry, rapt_token=RAPT_TOKEN)
+    serialized_token = token.Serialize()
+    LOG.debug('testSerialization: serialized_token=%s', serialized_token)
+
+    token2 = oauth2_client.AccessToken.UnSerialize(serialized_token)
+    self.assertEqual(token, token2)
+
+
+class FileSystemTokenCacheTest(unittest.TestCase):
+  """Unit tests for FileSystemTokenCache."""
+
+  def setUp(self):
+    self.cache = oauth2_client.FileSystemTokenCache()
+    self.start_time = datetime.datetime(2011, 3, 1, 10, 25, 13, 300826)
+    self.token_1 = oauth2_client.AccessToken(
+        'token1', self.start_time, rapt_token=RAPT_TOKEN)
+    self.token_2 = oauth2_client.AccessToken(
+        'token2', self.start_time + datetime.timedelta(seconds=492),
+        rapt_token=RAPT_TOKEN)
+    self.key = 'token1key'
+
+  def tearDown(self):
+    try:
+      os.unlink(self.cache.CacheFileName(self.key))
+    except:  # pylint: disable=bare-except
+      pass
+
+  def testPut(self):
+    self.cache.PutToken(self.key, self.token_1)
+    # Assert that the cache file exists and has correct permissions.
+    if not IS_WINDOWS:
+      self.assertEqual(
+          0o600,
+          stat.S_IMODE(os.stat(self.cache.CacheFileName(self.key)).st_mode))
+
+  def testPutGet(self):
+    """Tests putting and getting various tokens."""
+    # No cache file present.
+    self.assertEqual(None, self.cache.GetToken(self.key))
+
+    # Put a token
+    self.cache.PutToken(self.key, self.token_1)
+    cached_token = self.cache.GetToken(self.key)
+    self.assertEqual(self.token_1, cached_token)
+
+    # Put a different token
+    self.cache.PutToken(self.key, self.token_2)
+    cached_token = self.cache.GetToken(self.key)
+    self.assertEqual(self.token_2, cached_token)
+
+  def testGetBadFile(self):
+    f = open(self.cache.CacheFileName(self.key), 'w')
+    f.write('blah')
+    f.close()
+    self.assertEqual(None, self.cache.GetToken(self.key))
+
+  def testCacheFileName(self):
+    """Tests configuring the cache with a specific file name."""
+    cache = oauth2_client.FileSystemTokenCache(
+        path_pattern='/var/run/ccache/token.%(uid)s.%(key)s')
+    if IS_WINDOWS:
+      uid = '_'
+    else:
+      uid = os.getuid()
+    self.assertEqual('/var/run/ccache/token.%s.abc123' % uid,
+                     cache.CacheFileName('abc123'))
+
+    cache = oauth2_client.FileSystemTokenCache(
+        path_pattern='/var/run/ccache/token.%(key)s')
+    self.assertEqual('/var/run/ccache/token.abc123',
+                     cache.CacheFileName('abc123'))
+
+
+class RefreshTokenTest(unittest.TestCase):
+  """Unit tests for refresh tokens."""
+
+  def setUp(self):
+    self.mock_datetime = MockDateTime()
+    self.start_time = datetime.datetime(2011, 3, 1, 10, 25, 13, 300826)
+    self.mock_datetime.mock_now = self.start_time
+    self.client = CreateMockUserAccountClient(self.mock_datetime)
+
+  def testUniqeId(self):
+    cred_id = self.client.CacheKey()
+    self.assertEqual('0720afed6871f12761fbea3271f451e6ba184bf5', cred_id)
+
+  def testGetAuthorizationHeader(self):
+    self.assertEqual('Bearer %s' % ACCESS_TOKEN,
+                     self.client.GetAuthorizationHeader())
+
+
+class FakeResponse(object):
+
+  def __init__(self, status):
+    self._status = status
+
+  @property
+  def status(self):
+    return self._status
+
+
+class OAuth2GCEClientTest(unittest.TestCase):
+  """Unit tests for OAuth2GCEClient."""
+
+  def setUp(self):
+    patcher = mock.patch(
+        'gcs_oauth2_boto_plugin.oauth2_client.httplib2.Http', autospec=True)
+    self.addCleanup(patcher.stop)
+    self.mocked_http_class = patcher.start()
+    self.mock_http = self.mocked_http_class.return_value
+
+  @freeze_time('2014-03-26 01:01:01')
+  def testFetchAccessToken(self):
+    token = 'my_token'
+    self.mock_http.request.return_value = (
+        FakeResponse(200),
+        '{"access_token":"%(TOKEN)s", "expires_in": %(EXPIRES_IN)d}' % {
+            'TOKEN': token,
+            'EXPIRES_IN': 42
+        })
+
+    client = oauth2_client.OAuth2GCEClient()
+
+    self.assertEqual(
+        str(client.FetchAccessToken()),
+        'AccessToken(token=%s, expiry=2014-03-26 01:01:43Z)' % token)
+    self.mock_http.request.assert_called_with(
+        oauth2_client.META_TOKEN_URI,
+        method='GET',
+        body=None,
+        headers=oauth2_client.META_HEADERS)
+
+  def testIsGCENotFound(self):
+    self.mock_http.request.return_value = (FakeResponse(404), '')
+
+    self.assertFalse(oauth2_client._IsGCE())
+    self.mock_http.request.assert_called_once_with(
+        oauth2_client.METADATA_SERVER)
+
+  def testIsGCEServerNotFound(self):
+    self.mock_http.request.side_effect = httplib2.ServerNotFoundError()
+
+    self.assertFalse(oauth2_client._IsGCE())
+    self.mock_http.request.assert_called_once_with(
+        oauth2_client.METADATA_SERVER)
+
+  def testIsGCETrue(self):
+    self.mock_http.request.return_value = (FakeResponse(200), '')
+
+    self.assertTrue(oauth2_client._IsGCE())
+    self.mock_http.request.assert_called_once_with(
+        oauth2_client.METADATA_SERVER)
+
+
+if __name__ == '__main__':
+  unittest.main()

+ 11 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/requirements.txt

@@ -0,0 +1,11 @@
+boto>=2.29.1
+google-reauth>=0.1.0
+httplib2>=0.8
+oauth2client>=2.2.0
+pyOpenSSL>=0.13
+SocksiPy-branch==1.01
+retry_decorator>=1.0.0
+six>=1.6.1
+# Extra requirements only needed for testing, 'dev' label in setup.py.
+freezegun
+mock

+ 85 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/setup.py

@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 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.
+
+"""Setup installation module for gcs-oauth2-boto-plugin."""
+
+from setuptools import find_packages
+from setuptools import setup
+
+long_desc = """
+gcs-oauth2-boto-plugin is a Python application whose purpose is to behave as an
+auth plugin for the boto auth plugin framework for use with OAuth 2.0
+credentials for the Google Cloud Platform. This plugin is compatible with both
+user accounts and service accounts, and its functionality is essentially a
+wrapper around oauth2client with the addition of automatically caching tokens
+for the machine in a thread- and process-safe fashion.
+"""
+
+requires = [
+    'boto>=2.29.1',
+    'google-reauth>=0.1.0',
+    'httplib2>=0.8',
+    'oauth2client>=2.2.0',
+    'pyOpenSSL>=0.13',
+    # Not using 1.02 because of:
+    #   https://code.google.com/p/socksipy-branch/issues/detail?id=3
+    'SocksiPy-branch==1.01',
+    'retry_decorator>=1.0.0',
+    'six>=1.12.0'
+]
+
+extras_require = {
+    'dev': [
+        'freezegun',
+        'mock',
+    ],
+}
+
+setup(
+    name='gcs-oauth2-boto-plugin',
+    version='2.5',
+    url='https://developers.google.com/storage/docs/gspythonlibrary',
+    download_url=('https://github.com/GoogleCloudPlatform'
+                  '/gcs-oauth2-boto-plugin'),
+    license='Apache 2.0',
+    author='Google Inc.',
+    author_email='gs-team@google.com',
+    description=('Auth plugin allowing use the use of OAuth 2.0 credentials '
+                 'for Google Cloud Storage in the Boto library.'),
+    long_description=long_desc,
+    zip_safe=True,
+    platforms='any',
+    packages=find_packages(exclude=['third_party']),
+    include_package_data=True,
+    install_requires=requires,
+    extras_require=extras_require,
+    tests_require=extras_require['dev'],
+    test_suite='gcs_oauth2_boto_plugin.test_oauth2_client',
+    classifiers=[
+        'Development Status :: 7 - Inactive',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Apache Software License',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Topic :: Internet :: WWW/HTTP',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+)

+ 2 - 0
desktop/core/ext-py/gcs-oauth2-boto-plugin-2.5/test-requirements.txt

@@ -0,0 +1,2 @@
+mox>=0.5.3
+freezegun>=0.1.15

+ 12 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/.coveragerc

@@ -0,0 +1,12 @@
+[run]
+branch = True
+
+[report]
+fail_under = 100
+show_missing = True
+exclude_lines =
+    # Re-enable the standard pragma
+    pragma: NO COVER
+    # Ignore debug-only repr
+    def __repr__
+    pass

+ 36 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/.gitignore

@@ -0,0 +1,36 @@
+# Build artifacts
+*.py[cod]
+__pycache__
+*.egg-info/
+build/
+dist/
+
+# Documentation-related
+docs/_build
+
+# Test files
+.nox/
+.tox/
+.cache/
+
+# 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
+
+# Generated files
+pylintrc
+pylintrc.test

+ 21 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/.travis.yml

@@ -0,0 +1,21 @@
+language: python
+sudo: false
+matrix:
+  include:
+  - python: 3.6
+    env: NOX_SESSION=lint
+  - python: 2.7
+    env: NOX_SESSION="tests(py='2.7')"
+  - python: 3.4
+    env: NOX_SESSION="tests(py='3.4')"
+  - python: 3.5
+    env: NOX_SESSION="tests(py='3.5')"
+  - python: 3.6
+    env: NOX_SESSION="tests(py='3.6')"
+cache:
+  directories:
+  - ${HOME}/.cache
+install:
+- pip install --upgrade nox-automation
+script:
+- nox -s "$NOX_SESSION"

+ 7 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/CHANGELOG.rst

@@ -0,0 +1,7 @@
+Changelog
+=========
+
+v0.1.0
+------
+
+Initial release

+ 28 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/CONTRIBUTING.rst

@@ -0,0 +1,28 @@
+Contributing
+============
+
+#. **Please sign one of the contributor license agreements below.**
+#. Fork the repo, develop and test your code changes, add docs.
+#. Make sure that your commit messages clearly describe the changes.
+#. Send a pull request.
+
+Versioning
+----------
+
+This library follows `Semantic Versioning`_.
+
+.. _Semantic Versioning: http://semver.org/
+
+It is currently in major version zero (``0.y.z``), which means that anything
+may change at any time and the public API should not be considered
+stable.
+
+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 <https://developers.google.com/open-source/cla/individual>`__.
+- **If you work for a company that wants to allow you to contribute your work**, then you'll need to sign a `corporate CLA <https://developers.google.com/open-source/cla/corporate>`__.
+
+You can sign these electronically (just scroll to the bottom). After that, we'll be able to accept your pull requests.

+ 201 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.

+ 3 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/MANIFEST.in

@@ -0,0 +1,3 @@
+include README.rst LICENSE CHANGELOG.rst
+recursive-include tests *
+global-exclude *.pyc __pycache__

+ 39 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/README.rst

@@ -0,0 +1,39 @@
+Google Reauth Python Library
+============================
+
+|build| |docs| |pypi|
+
+This library provides Reauth support to Google's authentication libraries for
+Python. Reauth allows using two-factor authentication for end-user credentials.
+
+.. |build| image:: https://travis-ci.org/Google/google-reauth-python.svg?branch=master
+   :target: https://travis-ci.org/Google/google-reauth-python
+.. |pypi| image:: https://img.shields.io/pypi/v/google-reauth.svg
+   :target: https://pypi.python.org/pypi/google-reauth
+
+Installing
+----------
+
+You can install using `pip`_::
+
+    $ pip install google-reauth
+
+.. _pip: https://pip.pypa.io/en/stable/
+
+For more information on setting up your Python development environment, please refer to `Python Development Environment Setup Guide`_ for Google Cloud Platform.
+
+.. _`Python Development Environment Setup Guide`: https://cloud.google.com/python/setup
+
+Contributing
+------------
+
+See `CONTRIBUTING.rst`_ for more information on how to get started.
+
+.. _CONTRIBUTING.rst: https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/master/CONTRIBUTING.rst
+
+License
+-------
+
+Apache 2.0 - See `the LICENSE`_ for more information.
+
+.. _the LICENSE: https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/master/LICENSE

+ 0 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/__init__.py


+ 44 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/_helpers.py

@@ -0,0 +1,44 @@
+# Copyright 2017 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 getpass
+import sys
+
+
+def get_user_password(text):
+    """Get password from user.
+
+    Override this function with a different logic if you are using this library
+    outside a CLI.
+
+    Args:
+        text: message for the password prompt.
+
+    Returns: password string.
+    """
+    return getpass.getpass(text)
+
+
+def is_interactive():
+    """Check if we are in an interractive environment.
+
+    If the rapt token needs refreshing, the user needs to answer the
+    challenges.
+    If the user is not in an interractive environment, the challenges can not
+    be answered and we just wait for timeout for no reason.
+
+    Returns: True if is interactive environment, False otherwise.
+    """
+
+    return sys.stdin.isatty()

+ 152 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/_reauth_client.py

@@ -0,0 +1,152 @@
+# 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.
+
+"""Client for interacting with the Reauth HTTP API.
+
+This module provides the ability to do the following with the API:
+
+1. Get a list of challenges needed to obtain additional authorization.
+2. Send the result of the challenge to obtain a rapt token.
+3. A modified version of the standard OAuth2.0 refresh grant that takes a rapt
+   token.
+"""
+
+import json
+
+from six.moves import urllib
+
+from google_reauth import errors
+
+_REAUTH_API = 'https://reauth.googleapis.com/v2/sessions'
+
+
+def _handle_errors(msg):
+    """Raise an exception if msg has errors.
+
+    Args:
+        msg: parsed json from http response.
+
+    Returns: input response.
+    Raises: ReauthAPIError
+    """
+    if 'error' in msg:
+        raise errors.ReauthAPIError(msg['error']['message'])
+    return msg
+
+
+def _endpoint_request(http_request, path, body, access_token):
+    _, content = http_request(
+        uri='{0}{1}'.format(_REAUTH_API, path),
+        method='POST',
+        body=json.dumps(body),
+        headers={'Authorization': 'Bearer {0}'.format(access_token)}
+    )
+    response = json.loads(content)
+    _handle_errors(response)
+    return response
+
+
+def get_challenges(
+        http_request, supported_challenge_types, access_token,
+        requested_scopes=None):
+    """Does initial request to reauth API to get the challenges.
+
+    Args:
+        http_request (Callable): callable to run http requests. Accepts uri,
+            method, body and headers. Returns a tuple: (response, content)
+        supported_challenge_types (Sequence[str]): list of challenge names
+            supported by the manager.
+        access_token (str): Access token with reauth scopes.
+        requested_scopes (list[str]): Authorized scopes for the credentials.
+
+    Returns:
+        dict: The response from the reauth API.
+    """
+    body = {'supportedChallengeTypes': supported_challenge_types}
+    if requested_scopes:
+        body['oauthScopesForDomainPolicyLookup'] = requested_scopes
+
+    return _endpoint_request(
+        http_request, ':start', body, access_token)
+
+
+def send_challenge_result(
+        http_request, session_id, challenge_id, client_input, access_token):
+    """Attempt to refresh access token by sending next challenge result.
+
+    Args:
+        http_request (Callable): callable to run http requests. Accepts uri,
+            method, body and headers. Returns a tuple: (response, content)
+        session_id (str): session id returned by the initial reauth call.
+        challenge_id (str): challenge id returned by the initial reauth call.
+        client_input: dict with a challenge-specific client input. For example:
+            ``{'credential': password}`` for password challenge.
+        access_token (str): Access token with reauth scopes.
+
+    Returns:
+        dict: The response from the reauth API.
+    """
+    body = {
+        'sessionId': session_id,
+        'challengeId': challenge_id,
+        'action': 'RESPOND',
+        'proposalResponse': client_input,
+    }
+
+    return _endpoint_request(
+        http_request, '/{0}:continue'.format(session_id), body, access_token)
+
+
+def refresh_grant(
+        http_request, client_id, client_secret, refresh_token,
+        token_uri, scopes=None, rapt=None, headers={}):
+    """Implements the OAuth 2.0 Refresh Grant with the addition of the reauth
+    token.
+
+    Args:
+        http_request (Callable): callable to run http requests. Accepts uri,
+            method, body and headers. Returns a tuple: (response, content)
+        client_id (str): client id to get access token for reauth scope.
+        client_secret (str): client secret for the client_id
+        refresh_token (str): refresh token to refresh access token
+        token_uri (str): uri to refresh access token
+        scopes (str): scopes required by the client application as a
+            comma-joined list.
+        rapt (str): RAPT token
+        headers (dict): headers for http request
+
+    Returns:
+        Tuple[str, dict]: http response and parsed response content.
+    """
+    parameters = {
+        'grant_type': 'refresh_token',
+        'client_id': client_id,
+        'client_secret': client_secret,
+        'refresh_token': refresh_token,
+    }
+
+    if scopes:
+        parameters['scope'] = scopes
+
+    if rapt:
+        parameters['rapt'] = rapt
+
+    body = urllib.parse.urlencode(parameters)
+
+    response, content = http_request(
+        uri=token_uri,
+        method='POST',
+        body=body,
+        headers=headers)
+    return response, content

+ 132 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/challenges.py

@@ -0,0 +1,132 @@
+# Copyright 2017 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 abc
+import base64
+import sys
+
+import pyu2f.convenience.authenticator
+import pyu2f.errors
+import pyu2f.model
+import six
+
+from google_reauth import _helpers
+
+
+REAUTH_ORIGIN = 'https://accounts.google.com'
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ReauthChallenge(object):
+    """Base class for reauth challenges."""
+
+    @property
+    @abc.abstractmethod
+    def name(self):
+        """Returns the name of the challenge."""
+        pass
+
+    @property
+    @abc.abstractmethod
+    def is_locally_eligible(self):
+        """Returns true if a challenge is supported locally on this machine."""
+        pass
+
+    @abc.abstractmethod
+    def obtain_challenge_input(self, metadata):
+        """Performs logic required to obtain credentials and returns it.
+
+        Args:
+            metadata: challenge metadata returned in the 'challenges' field in
+                the initial reauth request. Includes the 'challengeType' field
+                and other challenge-specific fields.
+
+        Returns:
+            response that will be send to the reauth service as the content of
+            the 'proposalResponse' field in the request body. Usually a dict
+            with the keys specific to the challenge. For example,
+            {'credential': password} for password challenge.
+        """
+        pass
+
+
+class PasswordChallenge(ReauthChallenge):
+    """Challenge that asks for user's password."""
+
+    @property
+    def name(self):
+        return 'PASSWORD'
+
+    @property
+    def is_locally_eligible(self):
+        return True
+
+    def obtain_challenge_input(self, unused_metadata):
+        passwd = _helpers.get_user_password('Please enter your password:')
+        if not passwd:
+            passwd = ' '  # avoid the server crashing in case of no password :D
+        return {'credential': passwd}
+
+
+class SecurityKeyChallenge(ReauthChallenge):
+    """Challenge that asks for user's security key touch."""
+
+    @property
+    def name(self):
+        return 'SECURITY_KEY'
+
+    @property
+    def is_locally_eligible(self):
+        return True
+
+    def obtain_challenge_input(self, metadata):
+        sk = metadata['securityKey']
+        challenges = sk['challenges']
+        app_id = sk['applicationId']
+
+        challenge_data = []
+        for c in challenges:
+            kh = c['keyHandle'].encode('ascii')
+            key = pyu2f.model.RegisteredKey(
+                bytearray(base64.urlsafe_b64decode(kh)))
+            challenge = c['challenge'].encode('ascii')
+            challenge = base64.urlsafe_b64decode(challenge)
+            challenge_data.append({'key': key, 'challenge': challenge})
+
+        try:
+            api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
+                REAUTH_ORIGIN)
+            response = api.Authenticate(app_id, challenge_data,
+                                        print_callback=sys.stderr.write)
+            return {'securityKey': response}
+        except pyu2f.errors.U2FError as e:
+            if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
+                sys.stderr.write('Ineligible security key.\n')
+            elif e.code == pyu2f.errors.U2FError.TIMEOUT:
+                sys.stderr.write(
+                    'Timed out while waiting for security key touch.\n')
+            else:
+                raise e
+        except pyu2f.errors.NoDeviceFoundError:
+            sys.stderr.write('No security key found.\n')
+        return None
+
+
+AVAILABLE_CHALLENGES = {
+    challenge.name: challenge
+    for challenge in [
+        SecurityKeyChallenge(),
+        PasswordChallenge()
+    ]
+}

+ 62 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/errors.py

@@ -0,0 +1,62 @@
+# Copyright 2017 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 module that provides rapt authentication errors."""
+
+
+class ReauthError(Exception):
+    """Base exception for reauthentication."""
+    pass
+
+
+class HttpAccessTokenRefreshError(Exception):
+    """Error (with HTTP status) trying to refresh an expired access token."""
+    def __init__(self, message, status=None):
+        super(HttpAccessTokenRefreshError, self).__init__(message)
+        self.status = status
+
+
+class ReauthUnattendedError(ReauthError):
+    """An exception for when reauth cannot be answered."""
+
+    def __init__(self):
+        super(ReauthUnattendedError, self).__init__(
+            'Reauthentication challenge could not be answered because you are '
+            'not in an interactive session.')
+
+
+class ReauthFailError(ReauthError):
+    """An exception for when reauth failed."""
+
+    def __init__(self, message=None):
+        super(ReauthFailError, self).__init__(
+            'Reauthentication challenge failed. {0}'.format(message))
+
+
+class ReauthAPIError(ReauthError):
+    """An exception for when reauth API returned something we can't handle."""
+
+    def __init__(self, api_error):
+        super(ReauthAPIError, self).__init__(
+            'Reauthentication challenge failed due to API error: {0}.'.format(
+                api_error))
+
+
+class ReauthAccessTokenRefreshError(ReauthError):
+    """An exception for when we can't get an access token for reauth."""
+
+    def __init__(self, message=None, status=None):
+        super(ReauthAccessTokenRefreshError, self).__init__(
+            'Failed to get an access token for reauthentication. {0}'.format(
+                message))
+        self.status = status

+ 311 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/reauth.py

@@ -0,0 +1,311 @@
+# Copyright 2017 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 module that provides functions for handling rapt authentication.
+
+Reauth is a process of obtaining additional authentication (such as password,
+security token, etc.) while refreshing OAuth 2.0 credentials for a user.
+
+Credentials that use the Reauth flow must have the reauth scope,
+``https://www.googleapis.com/auth/accounts.reauth``.
+
+This module provides a high-level function for executing the Reauth process,
+:func:`refresh_access_token`, and lower-level helpers for doing the individual
+steps of the reauth process.
+
+Those steps are:
+
+1. Obtaining a list of challenges from the reauth server.
+2. Running through each challenge and sending the result back to the reauth
+   server.
+3. Refreshing the access token using the returned rapt token.
+"""
+
+import json
+import sys
+
+from six.moves import http_client
+from google_reauth import challenges
+from google_reauth import errors
+from google_reauth import _helpers
+from google_reauth import _reauth_client
+
+
+_REAUTH_SCOPE = 'https://www.googleapis.com/auth/accounts.reauth'
+
+_REAUTH_NEEDED_ERROR = 'invalid_grant'
+_REAUTH_NEEDED_ERROR_INVALID_RAPT = 'invalid_rapt'
+_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = 'rapt_required'
+
+_AUTHENTICATED = 'AUTHENTICATED'
+_CHALLENGE_REQUIRED = 'CHALLENGE_REQUIRED'
+_CHALLENGE_PENDING = 'CHALLENGE_PENDING'
+
+
+def _run_next_challenge(msg, http_request, access_token):
+    """Get the next challenge from msg and run it.
+
+    Args:
+        msg: Reauth API response body (either from the initial request to
+            https://reauth.googleapis.com/v2/sessions:start or from sending the
+            previous challenge response to
+            https://reauth.googleapis.com/v2/sessions/id:continue)
+        http_request: callable to run http requests. Accepts uri, method, body
+            and headers. Returns a tuple: (response, content)
+        access_token: reauth access token
+
+    Returns: rapt token.
+    Raises:
+        errors.ReauthError if reauth failed
+    """
+    for challenge in msg['challenges']:
+        if challenge['status'] != 'READY':
+            # Skip non-activated challneges.
+            continue
+        c = challenges.AVAILABLE_CHALLENGES.get(
+                challenge['challengeType'], None)
+        if not c:
+            raise errors.ReauthFailError(
+                'Unsupported challenge type {0}. Supported types: {0}'
+                .format(
+                    challenge['challengeType'],
+                    ','.join(challenges.AVAILABLE_CHALLENGES.keys())))
+        if not c.is_locally_eligible:
+            raise errors.ReauthFailError(
+                'Challenge {0} is not locally eligible'
+                .format(challenge['challengeType']))
+        client_input = c.obtain_challenge_input(challenge)
+        if not client_input:
+            return None
+        return _reauth_client.send_challenge_result(
+            http_request,
+            msg['sessionId'],
+            challenge['challengeId'],
+            client_input,
+            access_token)
+    return None
+
+
+def _obtain_rapt(http_request, access_token, requested_scopes, rounds_num=5):
+    """Given an http request method and reauth access token, get rapt token.
+
+    Args:
+        http_request: callable to run http requests. Accepts uri, method, body
+            and headers. Returns a tuple: (response, content)
+        access_token: reauth access token
+        requested_scopes: scopes required by the client application
+        rounds_num: max number of attempts to get a rapt after the next
+            challenge, before failing the reauth. This defines total number of
+            challenges + number of additional retries if the chalenge input
+            wasn't accepted.
+
+    Returns: rapt token.
+    Raises:
+        errors.ReauthError if reauth failed
+    """
+    msg = None
+
+    for _ in range(0, rounds_num):
+
+        if not msg:
+            msg = _reauth_client.get_challenges(
+                http_request,
+                list(challenges.AVAILABLE_CHALLENGES.keys()),
+                access_token,
+                requested_scopes)
+
+        if msg['status'] == _AUTHENTICATED:
+            return msg['encodedProofOfReauthToken']
+
+        if not (msg['status'] == _CHALLENGE_REQUIRED or
+                msg['status'] == _CHALLENGE_PENDING):
+            raise errors.ReauthAPIError(
+                'Challenge status {0}'.format(msg['status']))
+
+        if not _helpers.is_interactive():
+            raise errors.ReauthUnattendedError()
+
+        msg = _run_next_challenge(msg, http_request, access_token)
+
+    # If we got here it means we didn't get authenticated.
+    raise errors.ReauthFailError()
+
+
+def get_rapt_token(http_request, client_id, client_secret, refresh_token,
+                   token_uri, scopes=None):
+    """Given an http request method and refresh_token, get rapt token.
+
+    Args:
+        http_request: callable to run http requests. Accepts uri, method, body
+            and headers. Returns a tuple: (response, content)
+        client_id: client id to get access token for reauth scope.
+        client_secret: client secret for the client_id
+        refresh_token: refresh token to refresh access token
+        token_uri: uri to refresh access token
+        scopes: scopes required by the client application
+
+    Returns: rapt token.
+    Raises:
+        errors.ReauthError if reauth failed
+    """
+    sys.stderr.write('Reauthentication required.\n')
+
+    # Get access token for reauth.
+    response, content = _reauth_client.refresh_grant(
+        http_request=http_request,
+        client_id=client_id,
+        client_secret=client_secret,
+        refresh_token=refresh_token,
+        token_uri=token_uri,
+        scopes=_REAUTH_SCOPE,
+        headers={'Content-Type': 'application/x-www-form-urlencoded'})
+
+    try:
+        content = json.loads(content)
+    except (TypeError, ValueError):
+        raise errors.ReauthAccessTokenRefreshError(
+            'Invalid response {0}'.format(_substr_for_error_message(content)))
+
+    if response.status != http_client.OK:
+        raise errors.ReauthAccessTokenRefreshError(
+            _get_refresh_error_message(content), response.status)
+
+    if 'access_token' not in content:
+        raise errors.ReauthAccessTokenRefreshError(
+            'Access token missing from the response')
+
+    # Get rapt token from reauth API.
+    rapt_token = _obtain_rapt(
+        http_request,
+        content['access_token'],
+        requested_scopes=scopes)
+
+    return rapt_token
+
+
+def _rapt_refresh_required(content):
+    """Checks if the rapt refresh is required.
+
+    Args:
+        content: refresh response content
+
+    Returns:
+        True if rapt refresh is required.
+    """
+    try:
+        content = json.loads(content)
+    except (TypeError, ValueError):
+        return False
+    return (
+        content.get('error') == _REAUTH_NEEDED_ERROR and
+        (content.get('error_subtype') == _REAUTH_NEEDED_ERROR_INVALID_RAPT or
+         content.get('error_subtype') == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED))
+
+
+def _get_refresh_error_message(content):
+    """Constructs an error from the http response.
+
+    Args:
+        response: http response
+        content: parsed response content
+
+    Returns:
+        error message to show
+    """
+    error_msg = 'Invalid response.'
+    if 'error' in content:
+        error_msg = content['error']
+        if 'error_description' in content:
+            error_msg += ': ' + content['error_description']
+    return error_msg
+
+
+def _substr_for_error_message(content):
+    """Returns content string to include in the error message"""
+    return content if len(content) <= 100 else content[0:97] + "..."
+
+
+def refresh_access_token(
+        http_request, client_id, client_secret, refresh_token,
+        token_uri, rapt=None, scopes=None, headers=None):
+    """Refresh the access_token using the refresh_token.
+
+    Args:
+        http_request: callable to run http requests. Accepts uri, method, body
+            and headers. Returns a tuple: (response, content)
+        client_id: client id to get access token for reauth scope.
+        client_secret: client secret for the client_id
+        refresh_token: refresh token to refresh access token
+        token_uri: uri to refresh access token
+        scopes: scopes required by the client application
+
+    Returns:
+        Tuple[str, str, str, Optional[str], Optional[str], Optional[str]]: The
+            rapt token, the access token, new refresh token, expiration,
+            token id and response content returned by the token endpoint.
+    Raises:
+        errors.ReauthError if reauth failed
+        errors.HttpAccessTokenRefreshError it access token refresh failed
+    """
+
+    response, content = _reauth_client.refresh_grant(
+        http_request=http_request,
+        client_id=client_id,
+        client_secret=client_secret,
+        refresh_token=refresh_token,
+        token_uri=token_uri,
+        rapt=rapt,
+        headers=headers)
+
+    if response.status != http_client.OK:
+        # Check if we need a rapt token or if the rapt token is invalid.
+        # Once we refresh the rapt token, retry the access token refresh.
+        # If we did refresh the rapt token and still got an error, then the
+        # refresh token is expired or revoked.
+
+        if (_rapt_refresh_required(content)):
+            rapt = get_rapt_token(
+                http_request,
+                client_id,
+                client_secret,
+                refresh_token,
+                token_uri,
+                scopes=scopes,
+            )
+            # retry with refreshed rapt
+            response, content = _reauth_client.refresh_grant(
+                http_request=http_request,
+                client_id=client_id,
+                client_secret=client_secret,
+                refresh_token=refresh_token,
+                token_uri=token_uri,
+                rapt=rapt,
+                headers=headers)
+
+    try:
+        content = json.loads(content)
+    except (TypeError, ValueError):
+        raise errors.HttpAccessTokenRefreshError(
+            'Invalid response {0}'.format(_substr_for_error_message(content)),
+            response.status)
+
+    if response.status != http_client.OK:
+        raise errors.HttpAccessTokenRefreshError(
+            _get_refresh_error_message(content), response.status)
+
+    access_token = content['access_token']
+    refresh_token = content.get('refresh_token', None)
+    expires_in = content.get('expires_in', None)
+    id_token = content.get('id_token', None)
+    return rapt, content, access_token, refresh_token, expires_in, id_token

+ 147 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/google_reauth/reauth_creds.py

@@ -0,0 +1,147 @@
+# Copyright 2017 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.
+
+"""Two factor Oauth2Credentials."""
+
+
+import datetime
+import json
+import logging
+
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import transport
+from google_reauth import errors
+from google_reauth import reauth
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Oauth2WithReauthCredentials(client.OAuth2Credentials):
+    """Credentials object that extends OAuth2Credentials with reauth support.
+
+    This class provides the same functionality as OAuth2Credentials, but adds
+    the support for reauthentication and rapt tokens. These credentials should
+    behave the same as OAuth2Credentials when the credentials don't use rauth.
+    """
+
+    def __init__(self, *args, **kwargs):
+        """Create an instance of Oauth2WithReauthCredentials.
+
+        A Oauth2WithReauthCredentials has an extra rapt_token."""
+
+        self.rapt_token = kwargs.pop('rapt_token', None)
+        super(Oauth2WithReauthCredentials, self).__init__(*args, **kwargs)
+
+    @classmethod
+    def from_json(cls, json_data):
+        """Overrides."""
+
+        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'], client.EXPIRY_FORMAT)
+            except ValueError:
+                data['token_expiry'] = None
+
+        kwargs = {}
+        for param in ('revoke_uri', 'id_token', 'id_token_jwt',
+                      'token_response', 'scopes', 'token_info_uri',
+                      'rapt_token'):
+            value = data.get(param, None)
+            if value is not None:
+                kwargs[param] = value
+
+        retval = cls(
+          data['access_token'],
+          data['client_id'],
+          data['client_secret'],
+          data['refresh_token'],
+          data['token_expiry'],
+          data['token_uri'],
+          data['user_agent'],
+          **kwargs
+        )
+        retval.invalid = data['invalid']
+        return retval
+
+    @classmethod
+    def from_OAuth2Credentials(cls, original):
+        """Instantiate a Oauth2WithReauthCredentials from OAuth2Credentials."""
+        json = original.to_json()
+        return cls.from_json(json)
+
+    def _do_refresh_request(self, http):
+        """Refresh the access_token using the refresh_token.
+
+        Args:
+            http: An object to be used to make HTTP requests.
+            rapt_refreshed: If we did or did not already refreshed the rapt
+                            token.
+
+        Raises:
+            oauth2client.client.HttpAccessTokenRefreshError: if the refresh
+                fails.
+        """
+        headers = self._generate_refresh_request_headers()
+
+        _LOGGER.info('Refreshing access_token')
+
+        def http_request(uri, method, body, headers):
+            response, content = transport.request(
+                http, uri, method=method,
+                body=body, headers=headers)
+            content = _helpers._from_bytes(content)
+            return response, content
+
+        try:
+            self._update(*reauth.refresh_access_token(
+                    http_request,
+                    self.client_id,
+                    self.client_secret,
+                    self.refresh_token,
+                    self.token_uri,
+                    rapt=self.rapt_token,
+                    scopes=list(self.scopes),
+                    headers=headers))
+        except (errors.ReauthAccessTokenRefreshError,
+                errors.HttpAccessTokenRefreshError) as e:
+            self.invalid = True
+            if self.store:
+                self.store.locked_put(self)
+            raise client.HttpAccessTokenRefreshError(e, status=e.status)
+
+    def _update(self, rapt, content, access_token, refresh_token=None,
+                expires_in=None, id_token=None):
+        if rapt:
+            self.rapt_token = rapt
+        self.token_response = content
+        self.access_token = access_token
+        self.refresh_token = (
+            refresh_token if refresh_token else self.refresh_token)
+        if expires_in:
+            delta = datetime.timedelta(seconds=int(expires_in))
+            self.token_expiry = delta + client._UTCNOW()
+        else:
+            self.token_expiry = None
+        self.id_token_jwt = id_token
+        self.id_token = (
+            client._extract_id_token(id_token) if id_token else None)
+
+        self.invalid = False
+        if self.store:
+            self.store.locked_put(self)

+ 42 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/nox.py

@@ -0,0 +1,42 @@
+# Copyright 2016 Google
+#
+# 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.
+
+from __future__ import absolute_import
+
+import nox
+
+
+@nox.session
+@nox.parametrize('py', ['2.7'])
+def tests(session, py):
+    session.interpreter = 'python{}'.format(py)
+    session.install('mock', 'pytest', 'pytest-cov')
+    session.install('-e', '.[oauth2client]')
+
+    session.run(
+        'py.test',
+        '--quiet',
+        '--cov=google_reauth',
+        '--cov-config=.coveragerc',
+        'tests',
+        *session.posargs
+    )
+
+
+@nox.session
+def lint(session):
+    session.install('flake8', 'docutils', 'pygments')
+    session.run('flake8', 'google_reauth')
+    session.run(
+        'python', 'setup.py', 'check', '--restructuredtext', '--strict')

+ 2 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/setup.cfg

@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1

+ 63 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/setup.py

@@ -0,0 +1,63 @@
+# 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.
+
+import io
+
+import setuptools
+
+
+DEPENDENCIES = ['pyu2f']
+
+OAUTH2CLIENT_EXTRA_DEPENDENCIES = [
+    'oauth2client>=2.0.0'
+]
+
+EXTRAS = {
+    'oauth2client': OAUTH2CLIENT_EXTRA_DEPENDENCIES
+}
+
+
+with io.open('README.rst', 'r') as fh:
+    long_description = fh.read()
+
+setuptools.setup(
+    name='google-reauth',
+    version='0.1.0',
+    author='Google',
+    author_email='googleapis-publisher@google.com',
+    description='Google Reauth Library',
+    long_description=long_description,
+    url='https://github.com/Google/google-reauth-python',
+    packages=setuptools.find_packages(exclude=('tests*', 'system_tests*')),
+    install_requires=DEPENDENCIES,
+    extras_require=EXTRAS,
+    license='Apache 2.0',
+    keywords='google auth oauth client reauth',
+    classifiers=(
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Apache Software License',
+        'Operating System :: POSIX',
+        'Operating System :: Microsoft :: Windows',
+        'Operating System :: MacOS :: MacOS X',
+        'Operating System :: OS Independent',
+        'Topic :: Internet :: WWW/HTTP',
+    ),
+)

+ 0 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/tests/__init__.py


+ 80 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/tests/test_challenges.py

@@ -0,0 +1,80 @@
+# 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 the reauth module."""
+
+import base64
+import json
+import os
+import unittest
+
+import mock
+
+from google_reauth import challenges
+
+import pyu2f
+
+
+class _U2FInterfaceMock(object):
+    def Authenticate(self, unused_app_id, challenge, unused_registered_keys):
+        raise self.error
+
+
+_u2f_interface_mock = _U2FInterfaceMock()
+
+
+class ChallengesTest(unittest.TestCase):
+    """This class contains tests for reauth challanges. """
+
+    @mock.patch('pyu2f.u2f.GetLocalU2FInterface', return_value = _u2f_interface_mock)
+    def testSecurityKeyError(self, u2f_mock):
+        metadata = {
+            'status': 'READY',
+            'challengeId': 2,
+            'challengeType': 'SECURITY_KEY',
+            'securityKey': {
+                'applicationId': 'security_key_application_id',
+                'challenges': [{
+                    'keyHandle': 'some_key',
+                    'challenge': base64.urlsafe_b64encode(
+                        'some_challenge'.encode('ascii')).decode('ascii'),
+                }]
+            }}
+
+        challenge = challenges.SecurityKeyChallenge()
+
+        _u2f_interface_mock.error = pyu2f.errors.U2FError(
+            pyu2f.errors.U2FError.DEVICE_INELIGIBLE)
+        self.assertEquals(None, challenge.obtain_challenge_input(metadata))
+
+        _u2f_interface_mock.error = pyu2f.errors.U2FError(
+            pyu2f.errors.U2FError.TIMEOUT)
+        self.assertEquals(None, challenge.obtain_challenge_input(metadata))
+
+        _u2f_interface_mock.error = pyu2f.errors.NoDeviceFoundError()
+        self.assertEquals(None, challenge.obtain_challenge_input(metadata))
+
+        _u2f_interface_mock.error = pyu2f.errors.U2FError(
+            pyu2f.errors.U2FError.BAD_REQUEST)
+        with self.assertRaises(pyu2f.errors.U2FError):
+            challenge.obtain_challenge_input(metadata)
+
+        _u2f_interface_mock.error = pyu2f.errors.UnsupportedVersionException()
+        with self.assertRaises(pyu2f.errors.UnsupportedVersionException):
+            challenge.obtain_challenge_input(metadata)
+
+    @mock.patch('getpass.getpass', return_value = None)
+    def testNoPassword(self, getpass_mock):
+        self.assertEquals(challenges.PasswordChallenge().obtain_challenge_input({}),
+            {'credential': ' '})

+ 382 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/tests/test_reauth.py

@@ -0,0 +1,382 @@
+# 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 the reauth module."""
+
+import base64
+import json
+import os
+import unittest
+
+import mock
+
+from six.moves import http_client
+from six.moves import urllib
+
+from oauth2client import client
+
+from google_reauth import challenges
+from google_reauth import reauth
+from google_reauth import errors
+from google_reauth import reauth_creds
+from google_reauth import _reauth_client
+from google_reauth.reauth_creds import Oauth2WithReauthCredentials
+
+from pyu2f import model
+from pyu2f import u2f
+
+
+_ok_response = mock.Mock(spec=['status'], status=http_client.OK)
+_error_response = mock.Mock(spec=['status'], status=None)
+
+
+class ReauthTest(unittest.TestCase):
+    """This class contains tests for mocking reauth.
+
+    The tests here are a bit more verbose since we are trying to mock out U2F, and
+    present the reauth code some form of valid reply. This makes this test a bit
+    of an integration test, as opposed to testing every individual method by
+    itself.
+    """
+
+    rapt_token = 'encoded_proof_of_reauth_token'
+    correct_password = 'correct_password'
+    oauth_api_url = 'http://some_url.com'
+    client_id = 'some_id'
+    client_secret = 'some_secret'
+
+    def _request_mock_side_effect(self, *args, **kwargs):
+        """Helper function to respond with valid requests as if a real server.
+
+        This is the helper function for mocking HTTP calls. The calls that should
+        end up here are to the oauth2 API or to the reauth API. The order of ifs
+        tries to mimic the real order that the requests are expected, but we do not
+        enforce a particular order so it can be more general.
+
+        Args:
+          *args: Every arg passed to a request.
+          **kwargs: Every keyed arg passed to a request.
+
+        Returns:
+          (str, str), Mocked (headers, content)
+
+        Raises:
+          Exception: In case this function doesn't know how to mock a request.
+        """
+
+        # Requests to oauth2 have the body urlencoded.
+        # Requests to reauth have a JSON body.
+        # Try to decode both and use as needed.
+        qp = dict(urllib.parse.parse_qsl(kwargs['body']))
+        try:
+            qp_json = json.loads(kwargs['body'])
+        except ValueError:
+            qp_json = {}
+
+        uri = kwargs['uri'] if 'uri' in kwargs else args[0]
+
+        # First call to oauth2 has REAUTH_SCOPE and returns an access token.
+        if ((uri == self.oauth_api_url and
+             qp.get('scope') == reauth._REAUTH_SCOPE)):
+            return _ok_response, json.dumps({'access_token': 'access_token_for_reauth'})
+
+        # Initialization call for reauth, serve first challenge
+        if uri == (_reauth_client._REAUTH_API + ':start'):
+            return None, json.dumps({
+                'status': 'CHALLENGE_REQUIRED',
+                'sessionId': 'session_id_1',
+                'challenges': [{
+                    'status': 'READY',
+                    'challengeId': 1,
+                    'challengeType': 'PASSWORD',
+                    'securityKey': {},
+                }],
+            })
+
+        # Continuation call for reauth, check first challenge and serve the second
+        if uri == (_reauth_client._REAUTH_API + '/session_id_1:continue'):
+            self.assertEqual(1, qp_json.get('challengeId'))
+            self.assertEqual('RESPOND', qp_json.get('action'))
+
+            if (qp_json.get('proposalResponse', {}).get('credential')
+                == self.correct_password):
+                # We got a correct password, go to security key
+                return None, json.dumps({
+                    'status': 'CHALLENGE_REQUIRED',
+                    'sessionId': 'session_id_2',
+                    'challenges': [{
+                        'status': 'READY',
+                        'challengeId': 2,
+                        'challengeType': 'SECURITY_KEY',
+                        'securityKey': {
+                            'applicationId': 'security_key_application_id',
+                            'challenges': [{
+                                'keyHandle': 'some_key',
+                                'challenge': base64.urlsafe_b64encode(
+                                    'some_challenge'.encode('ascii')).decode('ascii'),
+                            }],
+                        },
+                    }],
+                })
+            else:
+                # We got an incorrect password, ask again.
+                # Normally, the sessionID should be different, but for keeping this
+                # function simple, we are going to reuse session_id_1 to come back to
+                # this if block.
+                return None, json.dumps({
+                    'status': 'CHALLENGE_PENDING',
+                    'sessionId': 'session_id_1',
+                    'challenges': [{
+                        'status': 'READY',
+                        'challengeId': 1,
+                        'challengeType': 'PASSWORD',
+                        'securityKey': {},
+                    }],
+                })
+
+        # Continuation call for reauth, check second challenge and serve token
+        if uri == (_reauth_client._REAUTH_API + '/session_id_2:continue'):
+            self.assertEqual(2, qp_json.get('challengeId'))
+            self.assertEqual('RESPOND', qp_json.get('action'))
+            return None, json.dumps({
+                'status': 'AUTHENTICATED',
+                'sessionId': 'session_id_3',
+                'encodedProofOfReauthToken': self.rapt_token,
+            })
+
+        raise Exception(
+            'Unexpected call :/\nURL {0}\n{1}'.format(uri, kwargs['body']))
+
+  # This U2F mock is made by looking into the implementation of the class and
+  # making the minimum requirement to actually answer a challenge.
+    class _U2FInterfaceMock(object):
+
+        def Authenticate(self, unused_app_id, challenge, unused_registered_keys):
+            client_data = model.ClientData(
+                model.ClientData.TYP_AUTHENTICATION,
+                challenge,
+                'some_origin'.encode('ascii'))
+            return model.SignResponse('key_handle', 'resp', client_data)
+
+    def _call_reauth(self, request_mock, scopes=None):
+        if os.environ.get('SK_SIGNING_PLUGIN') is not None:
+            raise unittest.SkipTest('unset SK_SIGNING_PLUGIN.')
+        return reauth.get_rapt_token(
+            request_mock,
+            self.client_id,
+            self.client_secret,
+            'some_refresh_token',
+            self.oauth_api_url,
+            scopes=scopes)
+
+    def StartPatch(self, *args, **kwargs):
+        patcher = mock.patch(*args, **kwargs)
+        self.addCleanup(patcher.stop)
+        return patcher.start()
+
+  #######
+  # Helper functions and classes above.
+  # Actual tests below.
+  #######
+
+    def setUp(self):
+        self.u2f_local_interface_mock = self.StartPatch(
+            'pyu2f.u2f.GetLocalU2FInterface')
+        self.u2f_local_interface_mock.return_value = self._U2FInterfaceMock()
+
+        self.getpass_mock = self.StartPatch('getpass.getpass')
+        self.getpass_mock.return_value = self.correct_password
+
+        self.is_interactive_mock = self.StartPatch('sys.stdin')
+        self.is_interactive_mock.isatty = lambda: True
+
+    def testPassAndGnubbyReauth(self):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            reauth_result = self._call_reauth(request_mock)
+            self.assertEqual(self.rapt_token, reauth_result)
+            self.assertEqual(4, request_mock.call_count)
+
+    def testPassWithScopes(self):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            reauth_result = self._call_reauth(request_mock, [
+                'https://www.googleapis.com/auth/scope1',
+                'https://www.googleapis.com/auth/scope2'])
+            self.assertEqual(self.rapt_token, reauth_result)
+            self.assertEqual(4, request_mock.call_count)
+
+    def testIncorrectPassThenPassAndGnubbyReauth(self):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            self.getpass_mock.return_value = None
+            self.getpass_mock.side_effect = ['bad_pass', self.correct_password]
+            reauth_result = self._call_reauth(request_mock)
+            self.assertEqual(self.rapt_token, reauth_result)
+            self.assertEqual(5, request_mock.call_count)
+
+    def testNonInteractiveError(self):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            self.is_interactive_mock.isatty = lambda: False
+            with self.assertRaises(errors.ReauthUnattendedError):
+                unused_reauth_result = self._call_reauth(request_mock)
+
+    @mock.patch('google_reauth.challenges.AVAILABLE_CHALLENGES', {})
+    def testChallengeNotSupported(self):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            with self.assertRaises(errors.ReauthFailError):
+                reauth_result = self._call_reauth(request_mock)
+                self.assertEqual(self.rapt_token, reauth_result)
+                self.assertEqual(4, request_mock.call_count)
+
+    @mock.patch('google_reauth.challenges.PasswordChallenge.is_locally_eligible', False)
+    def testChallengeNotEligible(self):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            with self.assertRaises(errors.ReauthFailError):
+                reauth_result = self._call_reauth(request_mock)
+                self.assertEqual(self.rapt_token, reauth_result)
+                self.assertEqual(4, request_mock.call_count)
+
+    def accessTokenRefreshError(self, response, content):
+        def side_effect(*args, **kwargs):
+            uri = kwargs['uri'] if 'uri' in kwargs else args[0]
+            if (uri == self.oauth_api_url):
+                return response, content
+            raise Exception(
+                'Unexpected call :/\nURL {0}\n{1}'.format(uri, kwargs['body']))
+        with mock.patch('httplib2.Http.request',
+                        side_effect = side_effect) as request_mock:
+            with self.assertRaises(errors.HttpAccessTokenRefreshError):
+                reauth.refresh_access_token(
+                    request_mock,
+                    self.client_id,
+                    self.client_secret,
+                    'some_refresh_token',
+                    self.oauth_api_url)
+            self.assertEqual(1, request_mock.call_count)
+
+    def testAccessTokenRefreshError(self):
+        self.accessTokenRefreshError(_ok_response, "foo")
+        self.accessTokenRefreshError(_error_response, "foo")
+        self.accessTokenRefreshError(_error_response, json.dumps({
+            'error': 'non_reauth_error'}))
+
+    def reauthAccessTokenError(self, response, content):
+        def side_effect(*args, **kwargs):
+            qp = dict(urllib.parse.parse_qsl(kwargs['body']))
+            try:
+                qp_json = json.loads(kwargs['body'])
+            except ValueError:
+                qp_json = {}
+            uri = kwargs['uri'] if 'uri' in kwargs else args[0]
+            if ((uri == self.oauth_api_url and
+                 qp.get('scope') == reauth._REAUTH_SCOPE)):
+                return response, content
+            raise Exception(
+                'Unexpected call :/\nURL {0}\n{1}'.format(uri, kwargs['body']))
+        with mock.patch('httplib2.Http.request',
+                        side_effect = side_effect) as request_mock:
+            with self.assertRaises(errors.ReauthAccessTokenRefreshError):
+                reauth.get_rapt_token(
+                    request_mock,
+                    self.client_id,
+                    self.client_secret,
+                    'some_refresh_token',
+                    self.oauth_api_url)
+            self.assertEqual(1, request_mock.call_count)
+
+    def testReauthAccessTokenError(self):
+        self.reauthAccessTokenError(_ok_response, "foo")
+        self.reauthAccessTokenError(_ok_response, json.dumps({}))
+        self.reauthAccessTokenError(
+            _error_response, json.dumps({'error': 'some error'}))
+
+    def testResponseErrorMessage(self):
+        self.assertEqual('Invalid response.',
+            reauth._get_refresh_error_message({}))
+        self.assertEqual('There was an error.',
+            reauth._get_refresh_error_message({'error': 'There was an error.'}))
+        self.assertEqual('There was an error: error description',
+            reauth._get_refresh_error_message({
+                'error': 'There was an error',
+                'error_description': 'error description'}))
+
+    def getChallengesError(self, content):
+        def side_effect(*args, **kwargs):
+            uri = kwargs['uri'] if 'uri' in kwargs else args[0]
+            # First call to oauth2 has REAUTH_SCOPE and returns an access token.
+            if ((uri == self.oauth_api_url)):
+                return _ok_response, json.dumps({
+                    'access_token': 'access_token_for_reauth'})
+
+            # Initialization call for reauth, serve first challenge
+            if uri == (_reauth_client._REAUTH_API + ':start'):
+                return None, content
+
+        with mock.patch('httplib2.Http.request',
+                        side_effect = side_effect) as request_mock:
+            with self.assertRaises(errors.ReauthAPIError) as e:
+                reauth.get_rapt_token(
+                    request_mock,
+                    self.client_id,
+                    self.client_secret,
+                    'some_refresh_token',
+                    self.oauth_api_url)
+            self.assertEqual(2, request_mock.call_count)
+
+    def testGetChallengesError(self):
+        self.getChallengesError(json.dumps({'status': 'ERROR'}))
+        self.getChallengesError(json.dumps({
+            'error': {'message': 'Some error'}}))
+
+    def testChallangeNotActivatedError(self):
+        def side_effect(*args, **kwargs):
+            uri = kwargs['uri'] if 'uri' in kwargs else args[0]
+            # First call to oauth2 has REAUTH_SCOPE and returns an access token.
+            if ((uri == self.oauth_api_url)):
+                return _ok_response, json.dumps({
+                    'access_token': 'access_token_for_reauth'})
+
+            # Initialization call for reauth, serve first challenge
+            if uri == (_reauth_client._REAUTH_API + ':start'):
+                return None, json.dumps({
+                    'status': 'CHALLENGE_REQUIRED',
+                    'sessionId': 'session_id_1',
+                    'challenges': [{
+                        'status': 'NOT_READY',
+                        'challengeId': 1,
+                        'challengeType': 'PASSWORD'}]})
+
+        request_mock = self.StartPatch('httplib2.Http.request')
+        request_mock.side_effect = side_effect
+        with self.assertRaises(errors.ReauthFailError) as e:
+            reauth.get_rapt_token(
+                request_mock,
+                self.client_id,
+                self.client_secret,
+                'some_refresh_token',
+                self.oauth_api_url,
+                scopes=None)
+        self.assertEqual(6, request_mock.call_count)
+
+    @mock.patch('google_reauth.challenges.SecurityKeyChallenge.obtain_challenge_input', return_value = None)
+    def testRetryOnNoUserInput(self, challenge_mock):
+        with mock.patch('httplib2.Http.request',
+                        side_effect = self._request_mock_side_effect) as request_mock:
+            with self.assertRaises(errors.ReauthFailError):
+                reauth_result = self._call_reauth(request_mock)
+            self.assertEqual(7, request_mock.call_count)

+ 239 - 0
desktop/core/ext-py/google-reauth-python-0.1.0/tests/test_reauth_creds.py

@@ -0,0 +1,239 @@
+# 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 the reauth module."""
+
+import base64
+import datetime
+import json
+import os
+import unittest
+
+import mock
+from mock import patch
+
+from six.moves import http_client
+from six.moves import urllib
+
+from oauth2client import client
+from oauth2client import client
+
+from google_reauth import reauth
+from google_reauth import errors
+from google_reauth import reauth_creds
+from google_reauth import _reauth_client
+from google_reauth.reauth_creds import Oauth2WithReauthCredentials
+
+
+_ok_response = lambda: None
+setattr(_ok_response, 'status', http_client.OK)
+
+_error_response = lambda: None
+setattr(_error_response, 'status', None)
+
+_token_response = (
+    _ok_response,
+    json.dumps(
+        {'access_token': 'new_access_token', 'refresh_token': 'new_refresh_token',
+         'expires_in': 60 * 60}))
+
+
+class MockStore(client.Storage):
+    def __init__(self):
+        self.credentials = None
+
+    def locked_put(self, credentials):
+        self.credentials = credentials
+
+    def locked_get(self):
+        return self.credentials
+
+
+class ReauthCredsTest(unittest.TestCase):
+    """This class contains tests for Oauth2WithReauthCredentials. """
+
+    def StartPatch(self, *args, **kwargs):
+        patcher = mock.patch(*args, **kwargs)
+        self.addCleanup(patcher.stop)
+        return patcher.start()
+
+    @patch('google_reauth.reauth.get_rapt_token')
+    def get_rapt_token(self, *args, **kwargs):
+        return 'rapt_token'
+
+    def _http_mock(self, request_side_effect):
+        request_mock = self.StartPatch('httplib2.Http.request')
+        request_mock.side_effect = request_side_effect
+        http_mock = self.StartPatch('httplib2.Http')
+        http_mock.request = request_mock
+        return http_mock
+
+    def _run_refresh_test(self, http_mock, access_token, refresh_token,
+                          token_expiry, invalid):
+        creds = self._get_creds()
+        store = MockStore()
+        creds.set_store(store)
+
+        creds._do_refresh_request(http_mock)
+        self._check_credentials(creds, store, access_token, refresh_token,
+                                token_expiry, invalid)
+
+    def _get_creds(self):
+        return Oauth2WithReauthCredentials(
+            access_token='old_token', client_id='id', client_secret='secret',
+            refresh_token='old_refresh_token',
+            token_expiry=datetime.datetime(2018, 3, 2, 21, 26, 13),
+            token_uri='token_uri',
+            user_agent='user_agent')
+
+    def _check_credentials(self, creds, store, access_token, refresh_token,
+                           token_expiry, invalid):
+        stored_creds = store.locked_get() if store else creds
+
+        self.assertEqual(access_token, creds.access_token)
+        self.assertEqual(access_token, stored_creds.access_token)
+
+        self.assertEqual(refresh_token, creds.refresh_token)
+        self.assertEqual(refresh_token, stored_creds.refresh_token)
+
+        self.assertEqual(token_expiry, creds.token_expiry)
+        self.assertEqual(token_expiry, stored_creds.token_expiry)
+
+        self.assertEqual(invalid, creds.invalid)
+        self.assertEqual(invalid, stored_creds.invalid)
+
+  #######
+  # Helper functions and classes above.
+  # Actual tests below.
+  #######
+
+    def setUp(self):
+        get_rapt = self.StartPatch('google_reauth.reauth.get_rapt_token')
+        get_rapt.return_value='rapt_token'
+
+        current_datetime = self.StartPatch('oauth2client.client._UTCNOW')
+        current_datetime.return_value = datetime.datetime(2018, 3, 2, 21, 26, 13)
+
+    def testFromOAuth2Credentials(self):
+        orig = client.OAuth2Credentials(
+            access_token='at', client_id='ci', client_secret='cs',
+            refresh_token='rt', token_expiry='te', token_uri='tu',
+            user_agent='ua')
+        cred = Oauth2WithReauthCredentials.from_OAuth2Credentials(orig)
+        self.assertEqual('Oauth2WithReauthCredentials', cred.__class__.__name__)
+        self.assertEqual('ci', cred.client_id)
+        self.assertEqual('cs', cred.client_secret)
+
+    def testTokenExpiryFromJson(self):
+        cred = Oauth2WithReauthCredentials.from_json(json.dumps({
+            'access_token': 'access_token',
+            'client_id': 'client_id',
+            'client_secret': 'client_secret',
+            'refresh_token': 'refresh_token',
+            'token_expiry': 'foo',
+            'token_uri': 'token_uri',
+            'user_agent': 'user_agent',
+            'invalid': False}))
+        self.assertEqual(None, cred.token_expiry)
+        cred = Oauth2WithReauthCredentials.from_json(json.dumps({
+            'access_token': 'access_token',
+            'client_id': 'client_id',
+            'client_secret': 'client_secret',
+            'refresh_token': 'refresh_token',
+            'token_expiry': '2018-03-02T21:26:13Z',
+            'token_uri': 'token_uri',
+            'user_agent': 'user_agent',
+            'invalid': False}))
+        self.assertEqual(datetime.datetime(2018, 3, 2, 21, 26, 13),
+                         cred.token_expiry)
+
+    def testRefreshNoReauthRequired(self):
+        def request_side_effect(self, *args, **kwargs):
+            return _token_response
+
+        self._run_refresh_test(
+            self._http_mock(request_side_effect),
+            'new_access_token',
+            'new_refresh_token',
+            datetime.datetime(2018, 3, 2, 22, 26, 13),
+            False)
+
+    def testRefreshReauthRequired(self):
+        responses = [
+            _token_response,
+            (_error_response, json.dumps({
+                'error': 'invalid_grant',
+                'error_subtype': 'rapt_required'}))]
+        def request_side_effect(self, *args, **kwargs):
+            return responses.pop()
+
+        self._run_refresh_test(
+            self._http_mock(request_side_effect),
+            'new_access_token',
+            'new_refresh_token',
+            datetime.datetime(2018, 3, 2, 22, 26, 13),
+            False)
+
+    def testInvalidRapt(self):
+        responses = [
+            (_error_response, json.dumps({
+                'error': 'invalid_grant',
+                'error_subtype': 'rapt_required'})),
+            (_error_response, json.dumps({
+                'error': 'invalid_grant',
+                'error_subtype': 'rapt_required'}))]
+        def request_side_effect(self, *args, **kwargs):
+            return responses.pop()
+
+        creds = self._get_creds()
+        store = MockStore()
+        creds.set_store(store)
+
+        with self.assertRaises(client.HttpAccessTokenRefreshError):
+            creds._do_refresh_request(self._http_mock(request_side_effect))
+
+        self._check_credentials(
+            creds, store,
+            'old_token',
+            'old_refresh_token',
+            datetime.datetime(2018, 3, 2, 21, 26, 13),
+            True)
+
+    def testRefreshNoStore(self):
+        def request_side_effect(self, *args, **kwargs):
+            return _token_response
+
+        creds = self._get_creds()
+        creds._do_refresh_request(self._http_mock(request_side_effect))
+        self._check_credentials(
+            creds, None,
+            'new_access_token',
+            'new_refresh_token',
+            datetime.datetime(2018, 3, 2, 22, 26, 13),
+            False)
+
+    def testRefreshNoExpiry(self):
+        def request_side_effect(self, *args, **kwargs):
+            return (
+                _ok_response,
+                json.dumps(
+                    {'access_token': 'new_access_token',
+                     'refresh_token': 'new_refresh_token'}))
+
+        self._run_refresh_test(
+            self._http_mock(request_side_effect),
+            'new_access_token',
+            'new_refresh_token',
+            None,
+            False)

+ 16 - 0
desktop/core/ext-py/oauth2client-4.1.3/.coveragerc

@@ -0,0 +1,16 @@
+[run]
+branch = True
+
+[report]
+omit =
+    */samples/*
+    */conftest.py
+    # 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__

+ 3 - 0
desktop/core/ext-py/oauth2client-4.1.3/.github/ISSUE_TEMPLATE.md

@@ -0,0 +1,3 @@
+**Note**: oauth2client is now deprecated. As such, it is unlikely that we will
+address or respond to your issue. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).

+ 3 - 0
desktop/core/ext-py/oauth2client-4.1.3/.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,3 @@
+**Note**: oauth2client is now deprecated. As such, it is unlikely that we will
+review or merge to your pull request. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).

+ 31 - 0
desktop/core/ext-py/oauth2client-4.1.3/.gitignore

@@ -0,0 +1,31 @@
+# Build artifacts
+*.py[cod]
+oauth2client.egg-info/
+build/
+dist/
+
+# Documentation-related
+docs/_build
+/google_appengine/
+
+# Test files
+.tox/
+.cache/
+
+# 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

+ 48 - 0
desktop/core/ext-py/oauth2client-4.1.3/.travis.yml

@@ -0,0 +1,48 @@
+language: python
+sudo: false
+
+matrix:
+  include:
+  - python: 2.7
+    env: TOX_ENV=flake8
+  - python: 2.7
+    env: TOX_ENV=docs
+  - python: 2.7
+    env: TOX_ENV=gae
+  - python: 2.7
+    env: TOX_ENV=py27
+  - python: 3.4
+    env: TOX_ENV=py34
+  - python: 3.5
+    env: TOX_ENV=py35
+  - python: 2.7
+    env: TOX_ENV=system-tests
+  - python: 3.4
+    env: TOX_ENV=system-tests3
+  - python: 2.7
+    env: TOX_ENV=cover
+env:
+  global:
+  - GAE_PYTHONPATH=${HOME}/.cache/google_appengine
+cache:
+  directories:
+  - ${HOME}/.cache
+  - ${HOME}/.pyenv
+install:
+- ./scripts/install.sh
+script:
+- ./scripts/run.sh
+after_success:
+- if [[ "${TOX_ENV}" == "cover" ]]; then coveralls; fi
+notifications:
+  email: false
+
+deploy:
+  provider: pypi
+  user: gcloudpypi
+  distributions: sdist bdist_wheel
+  password:
+    secure: "C9ImNa5kbdnrQNfX9ww4PUtQIr3tN+nfxl7eDkP1B8Qr0QNYjrjov7x+DLImkKvmoJd3dxYtYIpLE9esObUHu0gKHYxqymNHtuAAyoBOUfPtmp0vIEse9brNKMtaey5Ngk7ZWz9EHKBBqRHxqgN+Giby+K9Ta3K3urJIq6urYhE="
+  on:
+    tags: true
+    repo: google/oauth2client

+ 504 - 0
desktop/core/ext-py/oauth2client-4.1.3/CHANGELOG.md

@@ -0,0 +1,504 @@
+# CHANGELOG
+
+## v4.1.3
+
+**Note**: oauth2client is deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+* Changed OAuth2 endpoints to use oauth2.googleapis.com variants. (#742)
+
+## v4.1.2
+
+**Note**: oauth2client is deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+Bug fixes:
+* Fix packaging issue had erroneously installed the test package. (#688)
+
+## v4.1.1
+
+**Note**: oauth2client is deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+New features:
+* Allow passing prompt='consent' via the flow_from_clientsecrets. (#717)
+
+## v4.1.0
+
+**Note**: oauth2client is now deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+New features:
+* Allow customizing the GCE metadata service address via an env var. (#704)
+* Store original encoded and signed identity JWT in OAuth2Credentials. (#680)
+* Use jsonpickle in django contrib, if available. (#676)
+
+Bug fixes:
+* Typo fixes. (#668, #697)
+* Remove b64 padding from PKCE values, per RFC7636. (#683)
+* Include LICENSE in Manifest.in. (#694)
+* Fix tests and CI. (#705, #712, #713)
+* Escape callback error code in flask_util. (#710)
+
+## v4.0.0
+
+New features:
+* New Django samples. (#636)
+* Add support for RFC7636 PKCE. (#588)
+* Release as a universal wheel. (#665)
+
+Bug fixes:
+* Fix django authorization redirect by correctly checking validity of credentials. (#651)
+* Correct query loss when using parse_qsl to dict. (#622)
+* Switch django models from pickle to jsonpickle. (#614)
+* Support new MIDDLEWARE Django 1.10 setting. (#623)
+* Remove usage of os.environ.setdefault. (#621)
+* Handle missing storage files correctly. (#576)
+* Try to revoke token with POST when getting a 405. (#662)
+
+Internal changes:
+* Use transport module for GCE environment check. (#612)
+* Remove __author__ lines and add contributors.md. (#627)
+* Clean up imports. (#625)
+* Use transport.request in tests. (#607)
+* Drop unittest2 dependency (#610)
+* Remove backslash line continuations. (#608)
+* Use transport helpers in system tests. (#606)
+* Clean up usage of HTTP mocks in tests. (#605)
+* Remove all uses of MagicMock. (#598)
+* Migrate test runner to pytest. (#569)
+* Merge util.py and _helpers.py. (#579)
+* Remove httplib2 imports from non-transport modules. (#577)
+
+Breaking changes:
+* Drop Python 3.3 support. (#603)
+* Drop Python 2.6 support. (#590)
+* Remove multistore_file. (#589)
+
+## 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

+ 43 - 0
desktop/core/ext-py/oauth2client-4.1.3/CODE_OF_CONDUCT.md

@@ -0,0 +1,43 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project,
+and in the interest of fostering an open and welcoming community,
+we pledge to respect all people who contribute through reporting issues,
+posting feature requests, updating documentation,
+submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project
+a harassment-free experience for everyone,
+regardless of level of experience, gender, gender identity and expression,
+sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information,
+such as physical or electronic
+addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct.
+By adopting this Code of Conduct,
+project maintainers commit themselves to fairly and consistently
+applying these principles to every aspect of managing this project.
+Project maintainers who do not follow or enforce the Code of Conduct
+may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior
+may be reported by opening an issue
+or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
+available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)

+ 202 - 0
desktop/core/ext-py/oauth2client-4.1.3/CONTRIBUTING.md

@@ -0,0 +1,202 @@
+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.
+
+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
+[GooglePythonStyle]: https://google.github.io/styleguide/pyguide.html
+[GitCommitRules]: http://chris.beams.io/posts/git-commit/#seven-rules

+ 95 - 0
desktop/core/ext-py/oauth2client-4.1.3/CONTRIBUTORS.md

@@ -0,0 +1,95 @@
+# Contribors to oauth2client
+
+## Maintainers
+
+* [Nathaniel Manista](https://github.com/nathanielmanistaatgoogle)
+* [Jon Wayne Parrott](https://github.com/jonparrott)
+* [Danny Hermes](https://github.com/dhermes)
+
+Previous maintainers:
+
+* [Craig Citro](https://github.com/craigcitro)
+* [Joe Gregorio](https://github.com/jcgregorio)
+
+## Contributors
+
+This list is generated from git commit authors.
+
+* aalexand <aalexand@google.com>
+* Aaron <aaronwinter@users.noreply.github.com>
+* Adam Chainz <me@adamj.eu>
+* ade@google.com
+* Alexandre Vivien <alx.vivien@gmail.com>
+* Ali Afshar <afshar@google.com>
+* Andrzej Pragacz <apragacz@o2.pl>
+* api.nickm@gmail.com
+* Ben Demaree <bendemaree@gmail.com>
+* Bill Prin <waprin@gmail.com, waprin@google.com>
+* Brendan McCollam <brendan@mccoll.am, bmccollam@uchicago.edu>
+* Craig Citro <craigcitro@gmail.com, craigcitro@google.com>
+* Dan Ring <dfring@gmail.com>
+* Daniel Hermes <dhermes@google.com, daniel.j.hermes@gmail.com>
+* Danilo Akamine <danilowz@gmail.com>
+* daryl herzmann <akrherz@iastate.edu>
+* dlorenc <lorenc.d@gmail.com>
+* Dominik Miedziński <dominik@mdzn.pl>
+* dr. Kertész Csaba-Zoltán <cskertesz@gmail.com>
+* Dustin Farris <dustin@dustinfarris.com>
+* Eddie Warner <happyspace@gmail.com>
+* Edwin Amsler <EdwinGuy@GMail.com>
+* elibixby <elibixby@google.com>
+* Emanuele Pucciarelli <ep@acm.org>
+* Eric Koleda <eric.koleda@google.com>
+* Frederik Creemers <frederikcreemers@gmail.com>
+* Guido van Rossum <guido@google.com>
+* Harsh Vardhan <harshvd95@gmail.com>
+* Herr Kaste <thdz.x@gmx.net>
+* INADA Naoki <inada-n@klab.com>
+* JacobMoshenko <moshenko@google.com>
+* Jay Lee <jay0lee@gmail.com>
+* Jed Hartman <jhartman@google.com>
+* Jeff Terrace <jterrace@gmail.com, jterrace@google.com>
+* Jeffrey Sorensen <sorensenjs@users.noreply.github.com>
+* Jeremi Joslin <jeremi@collabspot.com>
+* Jin Liu <liujin@google.com>
+* Joe Beda <jbeda@google.com>
+* Joe Gregorio <jcgregorio@google.com, joe.gregorio@gmail.com>
+* Johan Euphrosine <proppy@google.com>
+* John Asmuth <jasmuth@gmail.com, jasmuth@google.com>
+* John Vandenberg <jayvdb@gmail.com>
+* Jon Wayne Parrott <jon.wayne.parrott@gmail.com, jonwayne@google.com>
+* Jose Alcerreca <jalc@google.com>
+* KCs <cskertesz@gmail.com>
+* Keith Maxwell <keith.maxwell@gmail.com>
+* Ken Payson <kpayson@google.com>
+* Kevin Regan <regank@google.com>
+* lraccomando <lraccomando@gmail.com>
+* Luar Roji <cyberplant@users.noreply.github.com>
+* Luke Blanshard <leadpipe@google.com>
+* Marc Cohen <marccohen@google.com>
+* Mark Pellegrini <markpell@google.com>
+* Martin Trigaux <mat@odoo.com>
+* Matt McDonald <mmcdonald@google.com>
+* Nathan Naze <nanaze@gmail.com>
+* Nathaniel Manista <nathaniel@google.com>
+* Orest Bolohan <orest@google.com>
+* Pat Ferate <pferate@gmail.com>
+* Patrick Costello <pcostello@google.com>
+* Rafe Kaplan <rafek@google.com>
+* rahulpaul@google.com <rahulpaul@google.com>
+* RM Saksida <rsaksida@gmail.com>
+* Robert Kaplow <rkaplow@google.com>
+* Robert Spies <wilford@google.com>
+* Sergei Trofimovich <siarheit@google.com>
+* sgomes@google.com <sgomes@google.com>
+* Simon Cadman <src@niftiestsoftware.com>
+* soltanmm <soltanmm@users.noreply.github.com>
+* Sébastien de Melo <sebastien.de-melo@ubicast.eu>
+* takuya sato <sato-taku@klab.com>
+* thobrla <thobrla@google.com>
+* Tom Miller <tom.h.miller@gmail.com>
+* Tony Aiuto <aiuto@google.com>
+* Travis Hobrla <thobrla@google.com>
+* Veres Lajos <vlajos@gmail.com>
+* Vivek Seth <vivekseth.m@gmail.com>
+* Éamonn McManus <eamonn@mcmanus.net>

+ 210 - 0
desktop/core/ext-py/oauth2client-4.1.3/LICENSE

@@ -0,0 +1,210 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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:
+
+httplib2 - MIT License

+ 2 - 0
desktop/core/ext-py/oauth2client-4.1.3/MANIFEST.in

@@ -0,0 +1,2 @@
+include README.md LICENSE CHANGELOG.md
+recursive-include tests *

+ 5 - 0
desktop/core/ext-py/oauth2client-4.1.3/Makefile

@@ -0,0 +1,5 @@
+test:
+	tox
+
+docs:
+	scripts/doc-build

+ 33 - 0
desktop/core/ext-py/oauth2client-4.1.3/README.md

@@ -0,0 +1,33 @@
+[![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.
+
+**Note**: oauth2client is now deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). For more details on the deprecation, see [oauth2client deprecation](https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html).
+
+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.7 and 3.4+. More information [in the docs][2].
+
+[1]: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md
+[2]: https://oauth2client.readthedocs.io/#supported-python-versions

+ 177 - 0
desktop/core/ext-py/oauth2client-4.1.3/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."

BIN
desktop/core/ext-py/oauth2client-4.1.3/docs/_static/favicon.ico


BIN
desktop/core/ext-py/oauth2client-4.1.3/docs/_static/google_logo.png


+ 114 - 0
desktop/core/ext-py/oauth2client-4.1.3/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'),
+]

+ 138 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/index.rst

@@ -0,0 +1,138 @@
+oauth2client
+============
+
+.. note:: oauth2client is now deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+`google-auth`_ and `oauthlib`_. For more details on the deprecation, see `oauth2client deprecation`_.
+
+.. _google-auth: https://google-auth.readthedocs.io
+.. _oauthlib: http://oauthlib.readthedocs.io/
+.. _oauth2client deprecation: https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html
+
+*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.7 and 3.4+. (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.4. 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
+* Prominent projects like `django`_ have `dropped support`_ for earlier
+  versions (3.3 support dropped in December 2015, and 2.6 support
+  `dropped`_ in September 2014)
+
+.. _open-source projects: http://docs.python-requests.org/en/latest/
+.. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/
+.. _django: https://docs.djangoproject.com/
+.. _dropped support: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django
+.. _dropped: https://docs.djangoproject.com/en/1.7/faq/install/#what-python-version-can-i-use-with-django

+ 10 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/requirements.txt

@@ -0,0 +1,10 @@
+django
+flask
+keyring
+mock
+pycrypto>=2.6
+pyopenssl>=0.14
+python-gflags
+pyyaml
+webapp2
+WebOb

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.client.rst

@@ -0,0 +1,7 @@
+oauth2client\.client module
+===========================
+
+.. automodule:: oauth2client.client
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.clientsecrets.rst

@@ -0,0 +1,7 @@
+oauth2client\.clientsecrets module
+==================================
+
+.. automodule:: oauth2client.clientsecrets
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.appengine.rst

@@ -0,0 +1,7 @@
+oauth2client\.contrib\.appengine module
+=======================================
+
+.. automodule:: oauth2client.contrib.appengine
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.devshell.rst

@@ -0,0 +1,7 @@
+oauth2client\.contrib\.devshell module
+======================================
+
+.. automodule:: oauth2client.contrib.devshell
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 23 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.gce.rst

@@ -0,0 +1,7 @@
+oauth2client\.contrib\.gce module
+=================================
+
+.. automodule:: oauth2client.contrib.gce
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/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:

+ 32 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.rst

@@ -0,0 +1,32 @@
+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.multiprocess_file_storage
+   oauth2client.contrib.sqlalchemy
+   oauth2client.contrib.xsrfutil
+
+Module contents
+---------------
+
+.. automodule:: oauth2client.contrib
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.sqlalchemy.rst

@@ -0,0 +1,7 @@
+oauth2client\.contrib\.sqlalchemy module
+========================================
+
+.. automodule:: oauth2client.contrib.sqlalchemy
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.contrib.xsrfutil.rst

@@ -0,0 +1,7 @@
+oauth2client\.contrib\.xsrfutil module
+======================================
+
+.. automodule:: oauth2client.contrib.xsrfutil
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.crypt.rst

@@ -0,0 +1,7 @@
+oauth2client\.crypt module
+==========================
+
+.. automodule:: oauth2client.crypt
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.file.rst

@@ -0,0 +1,7 @@
+oauth2client\.file module
+=========================
+
+.. automodule:: oauth2client.file
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 30 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.rst

@@ -0,0 +1,30 @@
+oauth2client package
+====================
+
+Subpackages
+-----------
+
+.. toctree::
+
+    oauth2client.contrib
+
+Submodules
+----------
+
+.. toctree::
+
+   oauth2client.client
+   oauth2client.clientsecrets
+   oauth2client.crypt
+   oauth2client.file
+   oauth2client.service_account
+   oauth2client.tools
+   oauth2client.transport
+
+Module contents
+---------------
+
+.. automodule:: oauth2client
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.service_account.rst

@@ -0,0 +1,7 @@
+oauth2client\.service\_account module
+=====================================
+
+.. automodule:: oauth2client.service_account
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.tools.rst

@@ -0,0 +1,7 @@
+oauth2client\.tools module
+==========================
+
+.. automodule:: oauth2client.tools
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 7 - 0
desktop/core/ext-py/oauth2client-4.1.3/docs/source/oauth2client.transport.rst

@@ -0,0 +1,7 @@
+oauth2client\.transport module
+==============================
+
+.. automodule:: oauth2client.transport
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 24 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/__init__.py

@@ -0,0 +1,24 @@
+# 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__ = '4.1.3'
+
+GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
+GOOGLE_DEVICE_URI = 'https://oauth2.googleapis.com/device/code'
+GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'
+GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token'
+GOOGLE_TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo'
+

+ 341 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_helpers.py

@@ -0,0 +1,341 @@
+# 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 functools
+import inspect
+import json
+import logging
+import os
+import warnings
+
+import six
+from six.moves import urllib
+
+
+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
+
+_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
+_IS_DIR_MESSAGE = '{0}: Is a directory'
+_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
+
+
+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
+    ``_helpers.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
+                   _helpers.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 []
+    elif isinstance(scopes, six.string_types):
+        return scopes.split(' ')
+    else:
+        return scopes
+
+
+def parse_unique_urlencoded(content):
+    """Parses unique key-value parameters from urlencoded content.
+
+    Args:
+        content: string, URL-encoded key-value pairs.
+
+    Returns:
+        dict, The key-value pairs from ``content``.
+
+    Raises:
+        ValueError: if one of the keys is repeated.
+    """
+    urlencoded_params = urllib.parse.parse_qs(content)
+    params = {}
+    for key, value in six.iteritems(urlencoded_params):
+        if len(value) != 1:
+            msg = ('URL-encoded content contains a repeated value:'
+                   '%s -> %s' % (key, ', '.join(value)))
+            raise ValueError(msg)
+        params[key] = value[0]
+    return params
+
+
+def update_query_params(uri, params):
+    """Updates a URI with new query parameters.
+
+    If a given key from ``params`` is repeated in the ``uri``, then
+    the URI will be considered invalid and an error will occur.
+
+    If the URI is valid, then each value from ``params`` will
+    replace the corresponding value in the query parameters (if
+    it exists).
+
+    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 = parse_unique_urlencoded(parts.query)
+    query_params.update(params)
+    new_query = urllib.parse.urlencode(query_params)
+    new_parts = parts._replace(query=new_query)
+    return urllib.parse.urlunparse(new_parts)
+
+
+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:
+        return update_query_params(url, {name: value})
+
+
+def validate_file(filename):
+    if os.path.islink(filename):
+        raise IOError(_SYM_LINK_MESSAGE.format(filename))
+    elif os.path.isdir(filename):
+        raise IOError(_IS_DIR_MESSAGE.format(filename))
+    elif not os.path.isfile(filename):
+        warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
+
+
+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)

+ 136 - 0
desktop/core/ext-py/oauth2client-4.1.3/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())

+ 67 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/_pkce.py

@@ -0,0 +1,67 @@
+# 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.
+
+"""
+Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
+Public Clients
+
+See RFC7636.
+"""
+
+import base64
+import hashlib
+import os
+
+
+def code_verifier(n_bytes=64):
+    """
+    Generates a 'code_verifier' as described in section 4.1 of RFC 7636.
+
+    This is a 'high-entropy cryptographic random string' that will be
+    impractical for an attacker to guess.
+
+    Args:
+        n_bytes: integer between 31 and 96, inclusive. default: 64
+            number of bytes of entropy to include in verifier.
+
+    Returns:
+        Bytestring, representing urlsafe base64-encoded random data.
+    """
+    verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=')
+    # https://tools.ietf.org/html/rfc7636#section-4.1
+    # minimum length of 43 characters and a maximum length of 128 characters.
+    if len(verifier) < 43:
+        raise ValueError("Verifier too short. n_bytes must be > 30.")
+    elif len(verifier) > 128:
+        raise ValueError("Verifier too long. n_bytes must be < 97.")
+    else:
+        return verifier
+
+
+def code_challenge(verifier):
+    """
+    Creates a 'code_challenge' as described in section 4.2 of RFC 7636
+    by taking the sha256 hash of the verifier and then urlsafe
+    base64-encoding it.
+
+    Args:
+        verifier: bytestring, representing a code_verifier as generated by
+            code_verifier().
+
+    Returns:
+        Bytestring, representing a urlsafe base64-encoded sha256 hash digest,
+            without '=' padding.
+    """
+    digest = hashlib.sha256(verifier).digest()
+    return base64.urlsafe_b64encode(digest).rstrip(b'=')

+ 184 - 0
desktop/core/ext-py/oauth2client-4.1.3/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)

+ 124 - 0
desktop/core/ext-py/oauth2client-4.1.3/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)

+ 2170 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/client.py

@@ -0,0 +1,2170 @@
+# 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 _pkce
+from oauth2client import clientsecrets
+from oauth2client import transport
+
+
+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.getenv('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.getenv('GCE_METADATA_TIMEOUT', 3))
+except ValueError:  # pragma: NO COVER
+    GCE_METADATA_TIMEOUT = 3
+
+_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
+_GCE_METADATA_URI = 'http://' + os.getenv('GCE_METADATA_IP', '169.254.169.254')
+_METADATA_FLAVOR_HEADER = 'metadata-flavor'  # lowercase header
+_DESIRED_METADATA_FLAVOR = 'Google'
+_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
+
+# 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()
+
+
+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.
+    """
+
+    @_helpers.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, id_token_jwt=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.
+            id_token_jwt: string, the encoded and signed identity JWT. The
+                          decoded version of this is stored in id_token.
+
+        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.id_token_jwt = id_token_jwt
+        self.token_response = token_response
+        self.scopes = set(_helpers.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)
+
+    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)
+
+    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 = _helpers.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)
+        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),
+            id_token_jwt=data.get('id_token_jwt', 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):
+        """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: an object to be used to make HTTP requests.
+
+        Raises:
+            HttpAccessTokenRefreshError: When the refresh fails.
+        """
+        if not self.store:
+            self._do_refresh_request(http)
+        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)
+            finally:
+                self.store.release_lock()
+
+    def _do_refresh_request(self, http):
+        """Refresh the access_token using the refresh_token.
+
+        Args:
+            http: an object to be used to make HTTP requests.
+
+        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 = transport.request(
+            http, 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'])
+                self.id_token_jwt = d['id_token']
+            else:
+                self.id_token = None
+                self.id_token_jwt = 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):
+        """Revokes this credential and deletes the stored copy (if it exists).
+
+        Args:
+            http: an object to be used to make HTTP requests.
+        """
+        self._do_revoke(http, self.refresh_token or self.access_token)
+
+    def _do_revoke(self, http, token):
+        """Revokes this credential and deletes the stored copy (if it exists).
+
+        Args:
+            http: an object to be used to make HTTP requests.
+            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 = _helpers.update_query_params(
+            self.revoke_uri, query_params)
+        resp, content = transport.request(http, token_revoke_uri)
+        if resp.status == http_client.METHOD_NOT_ALLOWED:
+            body = urllib.parse.urlencode(query_params)
+            resp, content = transport.request(http, token_revoke_uri,
+                                              method='POST', body=body)
+        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):
+        """Retrieves the list of authorized scopes from the OAuth2 provider.
+
+        Args:
+            http: an object to be used to make HTTP requests.
+        """
+        self._do_retrieve_scopes(http, self.access_token)
+
+    def _do_retrieve_scopes(self, http, token):
+        """Retrieves the list of authorized scopes from the OAuth2 provider.
+
+        Args:
+            http: an object to be used to make HTTP requests.
+            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 = _helpers.update_query_params(
+            self.token_info_uri, query_params)
+        resp, content = transport.request(http, token_info_uri)
+        content = _helpers._from_bytes(content)
+        if resp.status == http_client.OK:
+            d = json.loads(content)
+            self.scopes = set(_helpers.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):
+        """Refreshes the access token.
+
+        Args:
+            http: unused HTTP object.
+
+        Raises:
+            AccessTokenCredentialsError: always
+        """
+        raise AccessTokenCredentialsError(
+            'The access_token is expired or invalid and can\'t be refreshed.')
+
+    def _revoke(self, http):
+        """Revokes the access_token and deletes the store if available.
+
+        Args:
+            http: an object to be used to make HTTP requests.
+        """
+        self._do_revoke(http, 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".
+    http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT)
+    try:
+        response, _ = transport.request(
+            http, _GCE_METADATA_URI, headers=_GCE_HEADERS)
+        return (
+            response.status == http_client.OK and
+            response.get(_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
+
+
+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.
+    """
+
+    @_helpers.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):
+        """Revokes the access_token and deletes the store if available.
+
+        Args:
+            http: an object to be used to make HTTP requests.
+        """
+        self._do_revoke(http, 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')
+
+
+@_helpers.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 = transport.request(http, 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 = _helpers.parse_unique_urlencoded(content)
+
+    # some providers respond with 'expires', others with 'expires_in'
+    if resp and 'expires' in resp:
+        resp['expires_in'] = resp.pop('expires')
+
+    return resp
+
+
+@_helpers.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,
+                          pkce=False,
+                          code_verifier=None):
+    """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.
+        pkce: boolean, default: False, Generate and include a "Proof Key
+              for Code Exchange" (PKCE) with your authorization and token
+              requests. This adds security for installed applications that
+              cannot protect a client_secret. See RFC 7636 for details.
+        code_verifier: bytestring or None, default: None, parameter passed
+                       as part of the code exchange when pkce=True. If
+                       None, a code_verifier will automatically be
+                       generated as part of step1_get_authorize_url(). See
+                       RFC 7636 for details.
+
+    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,
+                               pkce=pkce,
+                               code_verifier=code_verifier)
+
+    credentials = flow.step2_exchange(code, http=http)
+    return credentials
+
+
+@_helpers.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
+        pkce: boolean, default: False, Generate and include a "Proof Key
+              for Code Exchange" (PKCE) with your authorization and token
+              requests. This adds security for installed applications that
+              cannot protect a client_secret. See RFC 7636 for details.
+        code_verifier: bytestring or None, default: None, parameter passed
+                       as part of the code exchange when pkce=True. If
+                       None, a code_verifier will automatically be
+                       generated as part of step1_get_authorize_url(). See
+                       RFC 7636 for details.
+
+    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.
+    """
+
+    @_helpers.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,
+                 pkce=False,
+                 code_verifier=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.
+            pkce: boolean, default: False, Generate and include a "Proof Key
+                  for Code Exchange" (PKCE) with your authorization and token
+                  requests. This adds security for installed applications that
+                  cannot protect a client_secret. See RFC 7636 for details.
+            code_verifier: bytestring or None, default: None, parameter passed
+                           as part of the code exchange when pkce=True. If
+                           None, a code_verifier will automatically be
+                           generated as part of step1_get_authorize_url(). See
+                           RFC 7636 for details.
+            **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 = _helpers.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._pkce = pkce
+        self.code_verifier = code_verifier
+        self.params = _oauth2_web_server_flow_params(kwargs)
+
+    @_helpers.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
+        if self._pkce:
+            if not self.code_verifier:
+                self.code_verifier = _pkce.code_verifier()
+            challenge = _pkce.code_challenge(self.code_verifier)
+            query_params['code_challenge'] = challenge
+            query_params['code_challenge_method'] = 'S256'
+
+        query_params.update(self.params)
+        return _helpers.update_query_params(self.auth_uri, query_params)
+
+    @_helpers.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 = transport.request(
+            http, 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)
+
+    @_helpers.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 self._pkce:
+            post_data['code_verifier'] = self.code_verifier
+        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 = transport.request(
+            http, 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
+            id_token_jwt = None
+            if 'id_token' in d:
+                extracted_id_token = _extract_id_token(d['id_token'])
+                id_token_jwt = 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,
+                id_token_jwt=id_token_jwt, 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)
+
+
+@_helpers.positional(2)
+def flow_from_clientsecrets(filename, scope, redirect_uri=None,
+                            message=None, cache=None, login_hint=None,
+                            device_uri=None, pkce=None, code_verifier=None,
+                            prompt=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')
+            optional = (
+                'revoke_uri',
+                'device_uri',
+                'pkce',
+                'code_verifier',
+                'prompt'
+            )
+            for param in optional:
+                if locals()[param] is not None:
+                    constructor_kwargs[param] = locals()[param]
+
+            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))

+ 173 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/clientsecrets.py

@@ -0,0 +1,173 @@
+# 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
+
+
+# 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))

+ 6 - 0
desktop/core/ext-py/oauth2client-4.1.3/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.
+"""

+ 163 - 0
desktop/core/ext-py/oauth2client-4.1.3/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'

+ 118 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/_metadata.py

@@ -0,0 +1,118 @@
+# 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 os
+
+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 transport
+
+
+METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format(
+    os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal'))
+METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
+
+
+def get(http, path, root=METADATA_ROOT, recursive=None):
+    """Fetch a resource from the metadata server.
+
+    Args:
+        http: an object to be used to make HTTP requests.
+        path: A string indicating the resource to retrieve. For example,
+            'instance/service-accounts/default'
+        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:
+        http_client.HTTPException if an error corrured while
+        retrieving metadata.
+    """
+    url = urlparse.urljoin(root, path)
+    url = _helpers._add_query_parameter(url, 'recursive', recursive)
+
+    response, content = transport.request(
+        http, 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 http_client.HTTPException(
+            'Failed to retrieve {0} from the Google Compute Engine'
+            'metadata service. Response:\n{1}'.format(url, response))
+
+
+def get_service_account_info(http, service_account='default'):
+    """Get information about a service account from the metadata server.
+
+    Args:
+        http: an object to be used to make HTTP requests.
+        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.
+
+    Returns:
+         A dictionary with information about the specified service account,
+         for example:
+
+            {
+                'email': '...',
+                'scopes': ['scope', ...],
+                'aliases': ['default', '...']
+            }
+    """
+    return get(
+        http,
+        'instance/service-accounts/{0}/'.format(service_account),
+        recursive=True)
+
+
+def get_token(http, service_account='default'):
+    """Fetch an oauth token for the
+
+    Args:
+        http: an object to be used to make HTTP requests.
+        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.
+
+    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,
+        '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

+ 910 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/appengine.py

@@ -0,0 +1,910 @@
+# 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 webapp2 as webapp
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client import transport
+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
+
+
+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.
+    """
+
+    @_helpers.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 = _helpers.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):
+        """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: unused HTTP object
+
+        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.
+    """
+
+    @_helpers.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)
+
+    @_helpers.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 = _helpers.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(
+            transport.get_http_object(*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 = _helpers._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
+
+    """
+
+    @_helpers.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.'
+
+
+@_helpers.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)

+ 152 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/devshell.py

@@ -0,0 +1,152 @@
+# 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):
+        """Refreshes the access token.
+
+        Args:
+            http: unused HTTP object
+        """
+        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.')

+ 65 - 0
desktop/core/ext-py/oauth2client-4.1.3/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)

+ 489 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/__init__.py

@@ -0,0 +1,489 @@
+# 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.
+MIDDLEWARE or MIDDLEWARE_CLASSES (in Django  versions <1.10) should also
+contain the string 'django.contrib.sessions.middleware.SessionMiddleware'.
+
+
+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
+from six.moves.urllib import parse
+
+from oauth2client import clientsecrets
+from oauth2client import transport
+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)
+        info = _get_oauth2_client_id_and_secret(settings_instance)
+        self.client_id, self.client_secret = info
+
+        # Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE
+        middleware_settings = getattr(settings_instance, 'MIDDLEWARE', None)
+        if middleware_settings is None:
+            middleware_settings = getattr(
+                settings_instance, 'MIDDLEWARE_CLASSES', None)
+        if middleware_settings is None:
+            raise exceptions.ImproperlyConfigured(
+                'Django settings has neither MIDDLEWARE nor MIDDLEWARE_CLASSES'
+                'configured')
+
+        if ('django.contrib.sessions.middleware.SessionMiddleware' not in
+                middleware_settings):
+            raise exceptions.ImproperlyConfigured(
+                'The Google OAuth2 Helper requires session middleware to '
+                'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE '
+                '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: create HTTP client authorized with OAuth2 credentials."""
+        if self.has_credentials():
+            return self.credentials.authorize(transport.get_http_object())
+        return None

+ 32 - 0
desktop/core/ext-py/oauth2client-4.1.3/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"

+ 145 - 0
desktop/core/ext-py/oauth2client-4.1.3/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

+ 82 - 0
desktop/core/ext-py/oauth2client-4.1.3/oauth2client/contrib/django_util/models.py

@@ -0,0 +1,82 @@
+# 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 jsonpickle
+
+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:
+            try:
+                return jsonpickle.decode(
+                    base64.b64decode(encoding.smart_bytes(value)).decode())
+            except ValueError:
+                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(jsonpickle.encode(value).encode()))
+
+    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)

+ 28 - 0
desktop/core/ext-py/oauth2client-4.1.3/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"])

+ 26 - 0
desktop/core/ext-py/oauth2client-4.1.3/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")

+ 81 - 0
desktop/core/ext-py/oauth2client-4.1.3/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()

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません