aboutsummaryrefslogtreecommitdiff
path: root/oauth2client
diff options
context:
space:
mode:
authorcylan <cylan@google.com>2018-06-04 06:41:22 +0000
committercylan <cylan@google.com>2018-06-12 06:49:35 +0000
commit1af3acd3c375aa83b428a569b46c9c77cfca72c8 (patch)
tree2307fb4d96588f6957c6112c8861197ca8fa0f73 /oauth2client
parentea7213cdc8de40a4887a68aff85a1729884461d0 (diff)
parentae73312942d3cf0e98f097dfbb40f136c2a7c463 (diff)
downloadoauth2client-1af3acd3c375aa83b428a569b46c9c77cfca72c8.tar.gz
Merge commit 'ae733129' into oauth2client v3.0.0android-p-preview-5android-p-preview-4
Initial commitcd of oauth2client v3.0.0 with history. Added: - Android.bp - MODULE_LICENSE_APACHE2 - NOTICE - METADATA Bug: b/80314772 Test: pip uninstall oauth2client and complied acloud. Change-Id: Id17fea7b8b7bbe6e28a208a37cbfe509bc7a6af4
Diffstat (limited to 'oauth2client')
-rw-r--r--oauth2client/Android.bp38
-rw-r--r--oauth2client/__init__.py23
-rw-r--r--oauth2client/_helpers.py105
-rw-r--r--oauth2client/_openssl_crypt.py136
-rw-r--r--oauth2client/_pure_python_crypt.py184
-rw-r--r--oauth2client/_pycrypto_crypt.py124
-rw-r--r--oauth2client/client.py2133
-rw-r--r--oauth2client/clientsecrets.py174
-rw-r--r--oauth2client/contrib/__init__.py6
-rw-r--r--oauth2client/contrib/_appengine_ndb.py163
-rw-r--r--oauth2client/contrib/_fcntl_opener.py81
-rw-r--r--oauth2client/contrib/_metadata.py123
-rw-r--r--oauth2client/contrib/_win32_opener.py106
-rw-r--r--oauth2client/contrib/appengine.py913
-rw-r--r--oauth2client/contrib/devshell.py146
-rw-r--r--oauth2client/contrib/dictionary_storage.py65
-rw-r--r--oauth2client/contrib/django_util/__init__.py477
-rw-r--r--oauth2client/contrib/django_util/apps.py32
-rw-r--r--oauth2client/contrib/django_util/decorators.py145
-rw-r--r--oauth2client/contrib/django_util/models.py75
-rw-r--r--oauth2client/contrib/django_util/signals.py28
-rw-r--r--oauth2client/contrib/django_util/site.py26
-rw-r--r--oauth2client/contrib/django_util/storage.py81
-rw-r--r--oauth2client/contrib/django_util/views.py190
-rw-r--r--oauth2client/contrib/flask_util.py556
-rw-r--r--oauth2client/contrib/gce.py162
-rw-r--r--oauth2client/contrib/keyring_storage.py98
-rw-r--r--oauth2client/contrib/locked_file.py234
-rw-r--r--oauth2client/contrib/multiprocess_file_storage.py355
-rw-r--r--oauth2client/contrib/multistore_file.py505
-rw-r--r--oauth2client/contrib/sqlalchemy.py173
-rw-r--r--oauth2client/contrib/xsrfutil.py106
-rw-r--r--oauth2client/crypt.py250
-rw-r--r--oauth2client/file.py106
-rw-r--r--oauth2client/service_account.py673
-rw-r--r--oauth2client/tools.py256
-rw-r--r--oauth2client/transport.py245
-rw-r--r--oauth2client/util.py206
38 files changed, 9499 insertions, 0 deletions
diff --git a/oauth2client/Android.bp b/oauth2client/Android.bp
new file mode 100644
index 0000000..7818920
--- /dev/null
+++ b/oauth2client/Android.bp
@@ -0,0 +1,38 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+python_library {
+ name: "py-oauth2client",
+ host_supported: true,
+ srcs: [
+ "*.py",
+ "contrib/*.py",
+ "contrib/django_util/*.py",
+ ],
+ version: {
+ py2: {
+ enabled: true,
+ },
+ py3: {
+ enabled: true,
+ },
+ },
+ libs: [
+ "py-httplib2",
+ "py-pyasn1",
+ "py-pyasn1-modules",
+ "py-rsa",
+ "py-six",
+ ],
+ pkg_path: "oauth2client",
+}
diff --git a/oauth2client/__init__.py b/oauth2client/__init__.py
new file mode 100644
index 0000000..28384bb
--- /dev/null
+++ b/oauth2client/__init__.py
@@ -0,0 +1,23 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Client library for using OAuth2, especially with Google APIs."""
+
+__version__ = '3.0.0'
+
+GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
+GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
+GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
+GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
+GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
diff --git a/oauth2client/_helpers.py b/oauth2client/_helpers.py
new file mode 100644
index 0000000..cb959c5
--- /dev/null
+++ b/oauth2client/_helpers.py
@@ -0,0 +1,105 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Helper functions for commonly used utilities."""
+
+import base64
+import json
+
+import six
+
+
+def _parse_pem_key(raw_key_input):
+ """Identify and extract PEM keys.
+
+ Determines whether the given key is in the format of PEM key, and extracts
+ the relevant part of the key if it is.
+
+ Args:
+ raw_key_input: The contents of a private key file (either PEM or
+ PKCS12).
+
+ Returns:
+ string, The actual key if the contents are from a PEM file, or
+ else None.
+ """
+ offset = raw_key_input.find(b'-----BEGIN ')
+ if offset != -1:
+ return raw_key_input[offset:]
+
+
+def _json_encode(data):
+ return json.dumps(data, separators=(',', ':'))
+
+
+def _to_bytes(value, encoding='ascii'):
+ """Converts a string value to bytes, if necessary.
+
+ Unfortunately, ``six.b`` is insufficient for this task since in
+ Python2 it does not modify ``unicode`` objects.
+
+ Args:
+ value: The string/bytes value to be converted.
+ encoding: The encoding to use to convert unicode to bytes. Defaults
+ to "ascii", which will not allow any characters from ordinals
+ larger than 127. Other useful values are "latin-1", which
+ which will only allows byte ordinals (up to 255) and "utf-8",
+ which will encode any unicode that needs to be.
+
+ Returns:
+ The original value converted to bytes (if unicode) or as passed in
+ if it started out as bytes.
+
+ Raises:
+ ValueError if the value could not be converted to bytes.
+ """
+ result = (value.encode(encoding)
+ if isinstance(value, six.text_type) else value)
+ if isinstance(result, six.binary_type):
+ return result
+ else:
+ raise ValueError('{0!r} could not be converted to bytes'.format(value))
+
+
+def _from_bytes(value):
+ """Converts bytes to a string value, if necessary.
+
+ Args:
+ value: The string/bytes value to be converted.
+
+ Returns:
+ The original value converted to unicode (if bytes) or as passed in
+ if it started out as unicode.
+
+ Raises:
+ ValueError if the value could not be converted to unicode.
+ """
+ result = (value.decode('utf-8')
+ if isinstance(value, six.binary_type) else value)
+ if isinstance(result, six.text_type):
+ return result
+ else:
+ raise ValueError(
+ '{0!r} could not be converted to unicode'.format(value))
+
+
+def _urlsafe_b64encode(raw_bytes):
+ raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
+ return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
+
+
+def _urlsafe_b64decode(b64string):
+ # Guard against unicode strings, which base64 can't handle.
+ b64string = _to_bytes(b64string)
+ padded = b64string + b'=' * (4 - len(b64string) % 4)
+ return base64.urlsafe_b64decode(padded)
diff --git a/oauth2client/_openssl_crypt.py b/oauth2client/_openssl_crypt.py
new file mode 100644
index 0000000..77fac74
--- /dev/null
+++ b/oauth2client/_openssl_crypt.py
@@ -0,0 +1,136 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""OpenSSL Crypto-related routines for oauth2client."""
+
+from OpenSSL import crypto
+
+from oauth2client import _helpers
+
+
+class OpenSSLVerifier(object):
+ """Verifies the signature on a message."""
+
+ def __init__(self, pubkey):
+ """Constructor.
+
+ Args:
+ pubkey: OpenSSL.crypto.PKey, The public key to verify with.
+ """
+ self._pubkey = pubkey
+
+ def verify(self, message, signature):
+ """Verifies a message against a signature.
+
+ Args:
+ message: string or bytes, The message to verify. If string, will be
+ encoded to bytes as utf-8.
+ signature: string or bytes, The signature on the message. If string,
+ will be encoded to bytes as utf-8.
+
+ Returns:
+ True if message was signed by the private key associated with the
+ public key that this object was constructed with.
+ """
+ message = _helpers._to_bytes(message, encoding='utf-8')
+ signature = _helpers._to_bytes(signature, encoding='utf-8')
+ try:
+ crypto.verify(self._pubkey, signature, message, 'sha256')
+ return True
+ except crypto.Error:
+ return False
+
+ @staticmethod
+ def from_string(key_pem, is_x509_cert):
+ """Construct a Verified instance from a string.
+
+ Args:
+ key_pem: string, public key in PEM format.
+ is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
+ is expected to be an RSA key in PEM format.
+
+ Returns:
+ Verifier instance.
+
+ Raises:
+ OpenSSL.crypto.Error: if the key_pem can't be parsed.
+ """
+ key_pem = _helpers._to_bytes(key_pem)
+ if is_x509_cert:
+ pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
+ else:
+ pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
+ return OpenSSLVerifier(pubkey)
+
+
+class OpenSSLSigner(object):
+ """Signs messages with a private key."""
+
+ def __init__(self, pkey):
+ """Constructor.
+
+ Args:
+ pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
+ """
+ self._key = pkey
+
+ def sign(self, message):
+ """Signs a message.
+
+ Args:
+ message: bytes, Message to be signed.
+
+ Returns:
+ string, The signature of the message for the given key.
+ """
+ message = _helpers._to_bytes(message, encoding='utf-8')
+ return crypto.sign(self._key, message, 'sha256')
+
+ @staticmethod
+ def from_string(key, password=b'notasecret'):
+ """Construct a Signer instance from a string.
+
+ Args:
+ key: string, private key in PKCS12 or PEM format.
+ password: string, password for the private key file.
+
+ Returns:
+ Signer instance.
+
+ Raises:
+ OpenSSL.crypto.Error if the key can't be parsed.
+ """
+ key = _helpers._to_bytes(key)
+ parsed_pem_key = _helpers._parse_pem_key(key)
+ if parsed_pem_key:
+ pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
+ else:
+ password = _helpers._to_bytes(password, encoding='utf-8')
+ pkey = crypto.load_pkcs12(key, password).get_privatekey()
+ return OpenSSLSigner(pkey)
+
+
+def pkcs12_key_as_pem(private_key_bytes, private_key_password):
+ """Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
+
+ Args:
+ private_key_bytes: Bytes. PKCS#12 key in DER format.
+ private_key_password: String. Password for PKCS#12 key.
+
+ Returns:
+ String. PEM contents of ``private_key_bytes``.
+ """
+ private_key_password = _helpers._to_bytes(private_key_password)
+ pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
+ return crypto.dump_privatekey(crypto.FILETYPE_PEM,
+ pkcs12.get_privatekey())
diff --git a/oauth2client/_pure_python_crypt.py b/oauth2client/_pure_python_crypt.py
new file mode 100644
index 0000000..2c5d43a
--- /dev/null
+++ b/oauth2client/_pure_python_crypt.py
@@ -0,0 +1,184 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Pure Python crypto-related routines for oauth2client.
+
+Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
+to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
+certificates.
+"""
+
+from pyasn1.codec.der import decoder
+from pyasn1_modules import pem
+from pyasn1_modules.rfc2459 import Certificate
+from pyasn1_modules.rfc5208 import PrivateKeyInfo
+import rsa
+import six
+
+from oauth2client import _helpers
+
+
+_PKCS12_ERROR = r"""\
+PKCS12 format is not supported by the RSA library.
+Either install PyOpenSSL, or please convert .p12 format
+to .pem format:
+ $ cat key.p12 | \
+ > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
+ > openssl rsa > key.pem
+"""
+
+_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
+_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
+ '-----END RSA PRIVATE KEY-----')
+_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
+ '-----END PRIVATE KEY-----')
+_PKCS8_SPEC = PrivateKeyInfo()
+
+
+def _bit_list_to_bytes(bit_list):
+ """Converts an iterable of 1's and 0's to bytes.
+
+ Combines the list 8 at a time, treating each group of 8 bits
+ as a single byte.
+ """
+ num_bits = len(bit_list)
+ byte_vals = bytearray()
+ for start in six.moves.xrange(0, num_bits, 8):
+ curr_bits = bit_list[start:start + 8]
+ char_val = sum(val * digit
+ for val, digit in zip(_POW2, curr_bits))
+ byte_vals.append(char_val)
+ return bytes(byte_vals)
+
+
+class RsaVerifier(object):
+ """Verifies the signature on a message.
+
+ Args:
+ pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
+ """
+
+ def __init__(self, pubkey):
+ self._pubkey = pubkey
+
+ def verify(self, message, signature):
+ """Verifies a message against a signature.
+
+ Args:
+ message: string or bytes, The message to verify. If string, will be
+ encoded to bytes as utf-8.
+ signature: string or bytes, The signature on the message. If
+ string, will be encoded to bytes as utf-8.
+
+ Returns:
+ True if message was signed by the private key associated with the
+ public key that this object was constructed with.
+ """
+ message = _helpers._to_bytes(message, encoding='utf-8')
+ try:
+ return rsa.pkcs1.verify(message, signature, self._pubkey)
+ except (ValueError, rsa.pkcs1.VerificationError):
+ return False
+
+ @classmethod
+ def from_string(cls, key_pem, is_x509_cert):
+ """Construct an RsaVerifier instance from a string.
+
+ Args:
+ key_pem: string, public key in PEM format.
+ is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
+ is expected to be an RSA key in PEM format.
+
+ Returns:
+ RsaVerifier instance.
+
+ Raises:
+ ValueError: if the key_pem can't be parsed. In either case, error
+ will begin with 'No PEM start marker'. If
+ ``is_x509_cert`` is True, will fail to find the
+ "-----BEGIN CERTIFICATE-----" error, otherwise fails
+ to find "-----BEGIN RSA PUBLIC KEY-----".
+ """
+ key_pem = _helpers._to_bytes(key_pem)
+ if is_x509_cert:
+ der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
+ asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
+ if remaining != b'':
+ raise ValueError('Unused bytes', remaining)
+
+ cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
+ key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
+ pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
+ else:
+ pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
+ return cls(pubkey)
+
+
+class RsaSigner(object):
+ """Signs messages with a private key.
+
+ Args:
+ pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
+ """
+
+ def __init__(self, pkey):
+ self._key = pkey
+
+ def sign(self, message):
+ """Signs a message.
+
+ Args:
+ message: bytes, Message to be signed.
+
+ Returns:
+ string, The signature of the message for the given key.
+ """
+ message = _helpers._to_bytes(message, encoding='utf-8')
+ return rsa.pkcs1.sign(message, self._key, 'SHA-256')
+
+ @classmethod
+ def from_string(cls, key, password='notasecret'):
+ """Construct an RsaSigner instance from a string.
+
+ Args:
+ key: string, private key in PEM format.
+ password: string, password for private key file. Unused for PEM
+ files.
+
+ Returns:
+ RsaSigner instance.
+
+ Raises:
+ ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
+ PEM format.
+ """
+ key = _helpers._from_bytes(key) # pem expects str in Py3
+ marker_id, key_bytes = pem.readPemBlocksFromFile(
+ six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
+
+ if marker_id == 0:
+ pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
+ format='DER')
+ elif marker_id == 1:
+ key_info, remaining = decoder.decode(
+ key_bytes, asn1Spec=_PKCS8_SPEC)
+ if remaining != b'':
+ raise ValueError('Unused bytes', remaining)
+ pkey_info = key_info.getComponentByName('privateKey')
+ pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
+ format='DER')
+ else:
+ raise ValueError('No key could be detected.')
+
+ return cls(pkey)
diff --git a/oauth2client/_pycrypto_crypt.py b/oauth2client/_pycrypto_crypt.py
new file mode 100644
index 0000000..fd2ce0c
--- /dev/null
+++ b/oauth2client/_pycrypto_crypt.py
@@ -0,0 +1,124 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""pyCrypto Crypto-related routines for oauth2client."""
+
+from Crypto.Hash import SHA256
+from Crypto.PublicKey import RSA
+from Crypto.Signature import PKCS1_v1_5
+from Crypto.Util.asn1 import DerSequence
+
+from oauth2client import _helpers
+
+
+class PyCryptoVerifier(object):
+ """Verifies the signature on a message."""
+
+ def __init__(self, pubkey):
+ """Constructor.
+
+ Args:
+ pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
+ with.
+ """
+ self._pubkey = pubkey
+
+ def verify(self, message, signature):
+ """Verifies a message against a signature.
+
+ Args:
+ message: string or bytes, The message to verify. If string, will be
+ encoded to bytes as utf-8.
+ signature: string or bytes, The signature on the message.
+
+ Returns:
+ True if message was signed by the private key associated with the
+ public key that this object was constructed with.
+ """
+ message = _helpers._to_bytes(message, encoding='utf-8')
+ return PKCS1_v1_5.new(self._pubkey).verify(
+ SHA256.new(message), signature)
+
+ @staticmethod
+ def from_string(key_pem, is_x509_cert):
+ """Construct a Verified instance from a string.
+
+ Args:
+ key_pem: string, public key in PEM format.
+ is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
+ is expected to be an RSA key in PEM format.
+
+ Returns:
+ Verifier instance.
+ """
+ if is_x509_cert:
+ key_pem = _helpers._to_bytes(key_pem)
+ pemLines = key_pem.replace(b' ', b'').split()
+ certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
+ certSeq = DerSequence()
+ certSeq.decode(certDer)
+ tbsSeq = DerSequence()
+ tbsSeq.decode(certSeq[0])
+ pubkey = RSA.importKey(tbsSeq[6])
+ else:
+ pubkey = RSA.importKey(key_pem)
+ return PyCryptoVerifier(pubkey)
+
+
+class PyCryptoSigner(object):
+ """Signs messages with a private key."""
+
+ def __init__(self, pkey):
+ """Constructor.
+
+ Args:
+ pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
+ """
+ self._key = pkey
+
+ def sign(self, message):
+ """Signs a message.
+
+ Args:
+ message: string, Message to be signed.
+
+ Returns:
+ string, The signature of the message for the given key.
+ """
+ message = _helpers._to_bytes(message, encoding='utf-8')
+ return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
+
+ @staticmethod
+ def from_string(key, password='notasecret'):
+ """Construct a Signer instance from a string.
+
+ Args:
+ key: string, private key in PEM format.
+ password: string, password for private key file. Unused for PEM
+ files.
+
+ Returns:
+ Signer instance.
+
+ Raises:
+ NotImplementedError if the key isn't in PEM format.
+ """
+ parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
+ if parsed_pem_key:
+ pkey = RSA.importKey(parsed_pem_key)
+ else:
+ raise NotImplementedError(
+ 'No key in PEM format was detected. This implementation '
+ 'can only use the PyCrypto library for keys in PEM '
+ 'format.')
+ return PyCryptoSigner(pkey)
diff --git a/oauth2client/client.py b/oauth2client/client.py
new file mode 100644
index 0000000..8956443
--- /dev/null
+++ b/oauth2client/client.py
@@ -0,0 +1,2133 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""An OAuth 2.0 client.
+
+Tools for interacting with OAuth 2.0 protected resources.
+"""
+
+import collections
+import copy
+import datetime
+import json
+import logging
+import os
+import shutil
+import socket
+import sys
+import tempfile
+
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import clientsecrets
+from oauth2client import transport
+from oauth2client import util
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+HAS_OPENSSL = False
+HAS_CRYPTO = False
+try:
+ from oauth2client import crypt
+ HAS_CRYPTO = True
+ HAS_OPENSSL = crypt.OpenSSLVerifier is not None
+except ImportError: # pragma: NO COVER
+ pass
+
+
+logger = logging.getLogger(__name__)
+
+# Expiry is stored in RFC3339 UTC format
+EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+# Which certs to use to validate id_tokens received.
+ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
+# This symbol previously had a typo in the name; we keep the old name
+# around for now, but will remove it in the future.
+ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
+
+# Constant to use for the out of band OAuth 2.0 flow.
+OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
+
+# The value representing user credentials.
+AUTHORIZED_USER = 'authorized_user'
+
+# The value representing service account credentials.
+SERVICE_ACCOUNT = 'service_account'
+
+# The environment variable pointing the file with local
+# Application Default Credentials.
+GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
+# The ~/.config subdirectory containing gcloud credentials. Intended
+# to be swapped out in tests.
+_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
+# The environment variable name which can replace ~/.config if set.
+_CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG'
+
+# The error message we show users when we can't find the Application
+# Default Credentials.
+ADC_HELP_MSG = (
+ 'The Application Default Credentials are not available. They are '
+ 'available if running in Google Compute Engine. Otherwise, the '
+ 'environment variable ' +
+ GOOGLE_APPLICATION_CREDENTIALS +
+ ' must be defined pointing to a file defining the credentials. See '
+ 'https://developers.google.com/accounts/docs/'
+ 'application-default-credentials for more information.')
+
+_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
+
+# The access token along with the seconds in which it expires.
+AccessTokenInfo = collections.namedtuple(
+ 'AccessTokenInfo', ['access_token', 'expires_in'])
+
+DEFAULT_ENV_NAME = 'UNKNOWN'
+
+# If set to True _get_environment avoid GCE check (_detect_gce_environment)
+NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False')
+
+# Timeout in seconds to wait for the GCE metadata server when detecting the
+# GCE environment.
+try:
+ GCE_METADATA_TIMEOUT = int(
+ os.environ.setdefault('GCE_METADATA_TIMEOUT', '3'))
+except ValueError: # pragma: NO COVER
+ GCE_METADATA_TIMEOUT = 3
+
+_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
+_GCE_METADATA_HOST = '169.254.169.254'
+_METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
+_DESIRED_METADATA_FLAVOR = 'Google'
+
+# Expose utcnow() at module level to allow for
+# easier testing (by replacing with a stub).
+_UTCNOW = datetime.datetime.utcnow
+
+# NOTE: These names were previously defined in this module but have been
+# moved into `oauth2client.transport`,
+clean_headers = transport.clean_headers
+MemoryCache = transport.MemoryCache
+REFRESH_STATUS_CODES = transport.REFRESH_STATUS_CODES
+
+
+class SETTINGS(object):
+ """Settings namespace for globally defined values."""
+ env_name = None
+
+
+class Error(Exception):
+ """Base error for this module."""
+
+
+class FlowExchangeError(Error):
+ """Error trying to exchange an authorization grant for an access token."""
+
+
+class AccessTokenRefreshError(Error):
+ """Error trying to refresh an expired access token."""
+
+
+class HttpAccessTokenRefreshError(AccessTokenRefreshError):
+ """Error (with HTTP status) trying to refresh an expired access token."""
+ def __init__(self, *args, **kwargs):
+ super(HttpAccessTokenRefreshError, self).__init__(*args)
+ self.status = kwargs.get('status')
+
+
+class TokenRevokeError(Error):
+ """Error trying to revoke a token."""
+
+
+class UnknownClientSecretsFlowError(Error):
+ """The client secrets file called for an unknown type of OAuth 2.0 flow."""
+
+
+class AccessTokenCredentialsError(Error):
+ """Having only the access_token means no refresh is possible."""
+
+
+class VerifyJwtTokenError(Error):
+ """Could not retrieve certificates for validation."""
+
+
+class NonAsciiHeaderError(Error):
+ """Header names and values must be ASCII strings."""
+
+
+class ApplicationDefaultCredentialsError(Error):
+ """Error retrieving the Application Default Credentials."""
+
+
+class OAuth2DeviceCodeError(Error):
+ """Error trying to retrieve a device code."""
+
+
+class CryptoUnavailableError(Error, NotImplementedError):
+ """Raised when a crypto library is required, but none is available."""
+
+
+def _parse_expiry(expiry):
+ if expiry and isinstance(expiry, datetime.datetime):
+ return expiry.strftime(EXPIRY_FORMAT)
+ else:
+ return None
+
+
+class Credentials(object):
+ """Base class for all Credentials objects.
+
+ Subclasses must define an authorize() method that applies the credentials
+ to an HTTP transport.
+
+ Subclasses must also specify a classmethod named 'from_json' that takes a
+ JSON string as input and returns an instantiated Credentials object.
+ """
+
+ NON_SERIALIZED_MEMBERS = frozenset(['store'])
+
+ def authorize(self, http):
+ """Take an httplib2.Http instance (or equivalent) and authorizes it.
+
+ Authorizes it for the set of credentials, usually by replacing
+ http.request() with a method that adds in the appropriate headers and
+ then delegates to the original Http.request() method.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+ """
+ raise NotImplementedError
+
+ def refresh(self, http):
+ """Forces a refresh of the access_token.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+ """
+ raise NotImplementedError
+
+ def revoke(self, http):
+ """Revokes a refresh_token and makes the credentials void.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the revoke
+ request.
+ """
+ raise NotImplementedError
+
+ def apply(self, headers):
+ """Add the authorization to the headers.
+
+ Args:
+ headers: dict, the headers to add the Authorization header to.
+ """
+ raise NotImplementedError
+
+ def _to_json(self, strip, to_serialize=None):
+ """Utility function that creates JSON repr. of a Credentials object.
+
+ Args:
+ strip: array, An array of names of members to exclude from the
+ JSON.
+ to_serialize: dict, (Optional) The properties for this object
+ that will be serialized. This allows callers to
+ modify before serializing.
+
+ Returns:
+ string, a JSON representation of this instance, suitable to pass to
+ from_json().
+ """
+ curr_type = self.__class__
+ if to_serialize is None:
+ to_serialize = copy.copy(self.__dict__)
+ else:
+ # Assumes it is a str->str dictionary, so we don't deep copy.
+ to_serialize = copy.copy(to_serialize)
+ for member in strip:
+ if member in to_serialize:
+ del to_serialize[member]
+ to_serialize['token_expiry'] = _parse_expiry(
+ to_serialize.get('token_expiry'))
+ # Add in information we will need later to reconstitute this instance.
+ to_serialize['_class'] = curr_type.__name__
+ to_serialize['_module'] = curr_type.__module__
+ for key, val in to_serialize.items():
+ if isinstance(val, bytes):
+ to_serialize[key] = val.decode('utf-8')
+ if isinstance(val, set):
+ to_serialize[key] = list(val)
+ return json.dumps(to_serialize)
+
+ def to_json(self):
+ """Creating a JSON representation of an instance of Credentials.
+
+ Returns:
+ string, a JSON representation of this instance, suitable to pass to
+ from_json().
+ """
+ return self._to_json(self.NON_SERIALIZED_MEMBERS)
+
+ @classmethod
+ def new_from_json(cls, json_data):
+ """Utility class method to instantiate a Credentials subclass from JSON.
+
+ Expects the JSON string to have been produced by to_json().
+
+ Args:
+ json_data: string or bytes, JSON from to_json().
+
+ Returns:
+ An instance of the subclass of Credentials that was serialized with
+ to_json().
+ """
+ json_data_as_unicode = _helpers._from_bytes(json_data)
+ data = json.loads(json_data_as_unicode)
+ # Find and call the right classmethod from_json() to restore
+ # the object.
+ module_name = data['_module']
+ try:
+ module_obj = __import__(module_name)
+ except ImportError:
+ # In case there's an object from the old package structure,
+ # update it
+ module_name = module_name.replace('.googleapiclient', '')
+ module_obj = __import__(module_name)
+
+ module_obj = __import__(module_name,
+ fromlist=module_name.split('.')[:-1])
+ kls = getattr(module_obj, data['_class'])
+ return kls.from_json(json_data_as_unicode)
+
+ @classmethod
+ def from_json(cls, unused_data):
+ """Instantiate a Credentials object from a JSON description of it.
+
+ The JSON should have been produced by calling .to_json() on the object.
+
+ Args:
+ unused_data: dict, A deserialized JSON object.
+
+ Returns:
+ An instance of a Credentials subclass.
+ """
+ return Credentials()
+
+
+class Flow(object):
+ """Base class for all Flow objects."""
+ pass
+
+
+class Storage(object):
+ """Base class for all Storage objects.
+
+ Store and retrieve a single credential. This class supports locking
+ such that multiple processes and threads can operate on a single
+ store.
+ """
+ def __init__(self, lock=None):
+ """Create a Storage instance.
+
+ Args:
+ lock: An optional threading.Lock-like object. Must implement at
+ least acquire() and release(). Does not need to be
+ re-entrant.
+ """
+ self._lock = lock
+
+ def acquire_lock(self):
+ """Acquires any lock necessary to access this Storage.
+
+ This lock is not reentrant.
+ """
+ if self._lock is not None:
+ self._lock.acquire()
+
+ def release_lock(self):
+ """Release the Storage lock.
+
+ Trying to release a lock that isn't held will result in a
+ RuntimeError in the case of a threading.Lock or multiprocessing.Lock.
+ """
+ if self._lock is not None:
+ self._lock.release()
+
+ def locked_get(self):
+ """Retrieve credential.
+
+ The Storage lock must be held when this is called.
+
+ Returns:
+ oauth2client.client.Credentials
+ """
+ raise NotImplementedError
+
+ def locked_put(self, credentials):
+ """Write a credential.
+
+ The Storage lock must be held when this is called.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ raise NotImplementedError
+
+ def locked_delete(self):
+ """Delete a credential.
+
+ The Storage lock must be held when this is called.
+ """
+ raise NotImplementedError
+
+ def get(self):
+ """Retrieve credential.
+
+ The Storage lock must *not* be held when this is called.
+
+ Returns:
+ oauth2client.client.Credentials
+ """
+ self.acquire_lock()
+ try:
+ return self.locked_get()
+ finally:
+ self.release_lock()
+
+ def put(self, credentials):
+ """Write a credential.
+
+ The Storage lock must be held when this is called.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ self.acquire_lock()
+ try:
+ self.locked_put(credentials)
+ finally:
+ self.release_lock()
+
+ def delete(self):
+ """Delete credential.
+
+ Frees any resources associated with storing the credential.
+ The Storage lock must *not* be held when this is called.
+
+ Returns:
+ None
+ """
+ self.acquire_lock()
+ try:
+ return self.locked_delete()
+ finally:
+ self.release_lock()
+
+
+def _update_query_params(uri, params):
+ """Updates a URI with new query parameters.
+
+ Args:
+ uri: string, A valid URI, with potential existing query parameters.
+ params: dict, A dictionary of query parameters.
+
+ Returns:
+ The same URI but with the new query parameters added.
+ """
+ parts = urllib.parse.urlparse(uri)
+ query_params = dict(urllib.parse.parse_qsl(parts.query))
+ query_params.update(params)
+ new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
+ return urllib.parse.urlunparse(new_parts)
+
+
+class OAuth2Credentials(Credentials):
+ """Credentials object for OAuth 2.0.
+
+ Credentials can be applied to an httplib2.Http object using the authorize()
+ method, which then adds the OAuth 2.0 access token to each request.
+
+ OAuth2Credentials objects may be safely pickled and unpickled.
+ """
+
+ @util.positional(8)
+ def __init__(self, access_token, client_id, client_secret, refresh_token,
+ token_expiry, token_uri, user_agent, revoke_uri=None,
+ id_token=None, token_response=None, scopes=None,
+ token_info_uri=None):
+ """Create an instance of OAuth2Credentials.
+
+ This constructor is not usually called by the user, instead
+ OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
+
+ Args:
+ access_token: string, access token.
+ client_id: string, client identifier.
+ client_secret: string, client secret.
+ refresh_token: string, refresh token.
+ token_expiry: datetime, when the access_token expires.
+ token_uri: string, URI of token endpoint.
+ user_agent: string, The HTTP User-Agent to provide for this
+ application.
+ revoke_uri: string, URI for revoke endpoint. Defaults to None; a
+ token can't be revoked if this is None.
+ id_token: object, The identity of the resource owner.
+ token_response: dict, the decoded response to the token request.
+ None if a token hasn't been requested yet. Stored
+ because some providers (e.g. wordpress.com) include
+ extra fields that clients may want.
+ scopes: list, authorized scopes for these credentials.
+ token_info_uri: string, the URI for the token info endpoint. Defaults
+ to None; scopes can not be refreshed if this is None.
+
+ Notes:
+ store: callable, A callable that when passed a Credential
+ will store the credential back to where it came from.
+ This is needed to store the latest access_token if it
+ has expired and been refreshed.
+ """
+ self.access_token = access_token
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.refresh_token = refresh_token
+ self.store = None
+ self.token_expiry = token_expiry
+ self.token_uri = token_uri
+ self.user_agent = user_agent
+ self.revoke_uri = revoke_uri
+ self.id_token = id_token
+ self.token_response = token_response
+ self.scopes = set(util.string_to_scopes(scopes or []))
+ self.token_info_uri = token_info_uri
+
+ # True if the credentials have been revoked or expired and can't be
+ # refreshed.
+ self.invalid = False
+
+ def authorize(self, http):
+ """Authorize an httplib2.Http instance with these credentials.
+
+ The modified http.request method will add authentication headers to
+ each request and will refresh access_tokens when a 401 is received on a
+ request. In addition the http.request method has a credentials
+ property, http.request.credentials, which is the Credentials object
+ that authorized it.
+
+ Args:
+ http: An instance of ``httplib2.Http`` or something that acts
+ like it.
+
+ Returns:
+ A modified instance of http that was passed in.
+
+ Example::
+
+ h = httplib2.Http()
+ h = credentials.authorize(h)
+
+ You can't create a new OAuth subclass of httplib2.Authentication
+ because it never gets passed the absolute URI, which is needed for
+ signing. So instead we have to overload 'request' with a closure
+ that adds in the Authorization header and then calls the original
+ version of 'request()'.
+ """
+ transport.wrap_http_for_auth(self, http)
+ return http
+
+ def refresh(self, http):
+ """Forces a refresh of the access_token.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+ """
+ self._refresh(http.request)
+
+ def revoke(self, http):
+ """Revokes a refresh_token and makes the credentials void.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the revoke
+ request.
+ """
+ self._revoke(http.request)
+
+ def apply(self, headers):
+ """Add the authorization to the headers.
+
+ Args:
+ headers: dict, the headers to add the Authorization header to.
+ """
+ headers['Authorization'] = 'Bearer ' + self.access_token
+
+ def has_scopes(self, scopes):
+ """Verify that the credentials are authorized for the given scopes.
+
+ Returns True if the credentials authorized scopes contain all of the
+ scopes given.
+
+ Args:
+ scopes: list or string, the scopes to check.
+
+ Notes:
+ There are cases where the credentials are unaware of which scopes
+ are authorized. Notably, credentials obtained and stored before
+ this code was added will not have scopes, AccessTokenCredentials do
+ not have scopes. In both cases, you can use refresh_scopes() to
+ obtain the canonical set of scopes.
+ """
+ scopes = util.string_to_scopes(scopes)
+ return set(scopes).issubset(self.scopes)
+
+ def retrieve_scopes(self, http):
+ """Retrieves the canonical list of scopes for this access token.
+
+ Gets the scopes from the OAuth2 provider.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+
+ Returns:
+ A set of strings containing the canonical list of scopes.
+ """
+ self._retrieve_scopes(http.request)
+ return self.scopes
+
+ @classmethod
+ def from_json(cls, json_data):
+ """Instantiate a Credentials object from a JSON description of it.
+
+ The JSON should have been produced by calling .to_json() on the object.
+
+ Args:
+ json_data: string or bytes, JSON to deserialize.
+
+ Returns:
+ An instance of a Credentials subclass.
+ """
+ data = json.loads(_helpers._from_bytes(json_data))
+ if (data.get('token_expiry') and
+ not isinstance(data['token_expiry'], datetime.datetime)):
+ try:
+ data['token_expiry'] = datetime.datetime.strptime(
+ data['token_expiry'], EXPIRY_FORMAT)
+ except ValueError:
+ data['token_expiry'] = None
+ retval = cls(
+ data['access_token'],
+ data['client_id'],
+ data['client_secret'],
+ data['refresh_token'],
+ data['token_expiry'],
+ data['token_uri'],
+ data['user_agent'],
+ revoke_uri=data.get('revoke_uri', None),
+ id_token=data.get('id_token', None),
+ token_response=data.get('token_response', None),
+ scopes=data.get('scopes', None),
+ token_info_uri=data.get('token_info_uri', None))
+ retval.invalid = data['invalid']
+ return retval
+
+ @property
+ def access_token_expired(self):
+ """True if the credential is expired or invalid.
+
+ If the token_expiry isn't set, we assume the token doesn't expire.
+ """
+ if self.invalid:
+ return True
+
+ if not self.token_expiry:
+ return False
+
+ now = _UTCNOW()
+ if now >= self.token_expiry:
+ logger.info('access_token is expired. Now: %s, token_expiry: %s',
+ now, self.token_expiry)
+ return True
+ return False
+
+ def get_access_token(self, http=None):
+ """Return the access token and its expiration information.
+
+ If the token does not exist, get one.
+ If the token expired, refresh it.
+ """
+ if not self.access_token or self.access_token_expired:
+ if not http:
+ http = transport.get_http_object()
+ self.refresh(http)
+ return AccessTokenInfo(access_token=self.access_token,
+ expires_in=self._expires_in())
+
+ def set_store(self, store):
+ """Set the Storage for the credential.
+
+ Args:
+ store: Storage, an implementation of Storage object.
+ This is needed to store the latest access_token if it
+ has expired and been refreshed. This implementation uses
+ locking to check for updates before updating the
+ access_token.
+ """
+ self.store = store
+
+ def _expires_in(self):
+ """Return the number of seconds until this token expires.
+
+ If token_expiry is in the past, this method will return 0, meaning the
+ token has already expired.
+
+ If token_expiry is None, this method will return None. Note that
+ returning 0 in such a case would not be fair: the token may still be
+ valid; we just don't know anything about it.
+ """
+ if self.token_expiry:
+ now = _UTCNOW()
+ if self.token_expiry > now:
+ time_delta = self.token_expiry - now
+ # TODO(orestica): return time_delta.total_seconds()
+ # once dropping support for Python 2.6
+ return time_delta.days * 86400 + time_delta.seconds
+ else:
+ return 0
+
+ def _updateFromCredential(self, other):
+ """Update this Credential from another instance."""
+ self.__dict__.update(other.__getstate__())
+
+ def __getstate__(self):
+ """Trim the state down to something that can be pickled."""
+ d = copy.copy(self.__dict__)
+ del d['store']
+ return d
+
+ def __setstate__(self, state):
+ """Reconstitute the state of the object from being pickled."""
+ self.__dict__.update(state)
+ self.store = None
+
+ def _generate_refresh_request_body(self):
+ """Generate the body that will be used in the refresh request."""
+ body = urllib.parse.urlencode({
+ 'grant_type': 'refresh_token',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'refresh_token': self.refresh_token,
+ })
+ return body
+
+ def _generate_refresh_request_headers(self):
+ """Generate the headers that will be used in the refresh request."""
+ headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+
+ if self.user_agent is not None:
+ headers['user-agent'] = self.user_agent
+
+ return headers
+
+ def _refresh(self, http_request):
+ """Refreshes the access_token.
+
+ This method first checks by reading the Storage object if available.
+ If a refresh is still needed, it holds the Storage lock until the
+ refresh is completed.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ refresh request.
+
+ Raises:
+ HttpAccessTokenRefreshError: When the refresh fails.
+ """
+ if not self.store:
+ self._do_refresh_request(http_request)
+ else:
+ self.store.acquire_lock()
+ try:
+ new_cred = self.store.locked_get()
+
+ if (new_cred and not new_cred.invalid and
+ new_cred.access_token != self.access_token and
+ not new_cred.access_token_expired):
+ logger.info('Updated access_token read from Storage')
+ self._updateFromCredential(new_cred)
+ else:
+ self._do_refresh_request(http_request)
+ finally:
+ self.store.release_lock()
+
+ def _do_refresh_request(self, http_request):
+ """Refresh the access_token using the refresh_token.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ refresh request.
+
+ Raises:
+ HttpAccessTokenRefreshError: When the refresh fails.
+ """
+ body = self._generate_refresh_request_body()
+ headers = self._generate_refresh_request_headers()
+
+ logger.info('Refreshing access_token')
+ resp, content = http_request(
+ self.token_uri, method='POST', body=body, headers=headers)
+ content = _helpers._from_bytes(content)
+ if resp.status == http_client.OK:
+ d = json.loads(content)
+ self.token_response = d
+ self.access_token = d['access_token']
+ self.refresh_token = d.get('refresh_token', self.refresh_token)
+ if 'expires_in' in d:
+ delta = datetime.timedelta(seconds=int(d['expires_in']))
+ self.token_expiry = delta + _UTCNOW()
+ else:
+ self.token_expiry = None
+ if 'id_token' in d:
+ self.id_token = _extract_id_token(d['id_token'])
+ else:
+ self.id_token = None
+ # On temporary refresh errors, the user does not actually have to
+ # re-authorize, so we unflag here.
+ self.invalid = False
+ if self.store:
+ self.store.locked_put(self)
+ else:
+ # An {'error':...} response body means the token is expired or
+ # revoked, so we flag the credentials as such.
+ logger.info('Failed to retrieve access token: %s', content)
+ error_msg = 'Invalid response {0}.'.format(resp['status'])
+ try:
+ d = json.loads(content)
+ if 'error' in d:
+ error_msg = d['error']
+ if 'error_description' in d:
+ error_msg += ': ' + d['error_description']
+ self.invalid = True
+ if self.store is not None:
+ self.store.locked_put(self)
+ except (TypeError, ValueError):
+ pass
+ raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
+
+ def _revoke(self, http_request):
+ """Revokes this credential and deletes the stored copy (if it exists).
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ revoke request.
+ """
+ self._do_revoke(http_request, self.refresh_token or self.access_token)
+
+ def _do_revoke(self, http_request, token):
+ """Revokes this credential and deletes the stored copy (if it exists).
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ refresh request.
+ token: A string used as the token to be revoked. Can be either an
+ access_token or refresh_token.
+
+ Raises:
+ TokenRevokeError: If the revoke request does not return with a
+ 200 OK.
+ """
+ logger.info('Revoking token')
+ query_params = {'token': token}
+ token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
+ resp, content = http_request(token_revoke_uri)
+ if resp.status == http_client.OK:
+ self.invalid = True
+ else:
+ error_msg = 'Invalid response {0}.'.format(resp.status)
+ try:
+ d = json.loads(_helpers._from_bytes(content))
+ if 'error' in d:
+ error_msg = d['error']
+ except (TypeError, ValueError):
+ pass
+ raise TokenRevokeError(error_msg)
+
+ if self.store:
+ self.store.delete()
+
+ def _retrieve_scopes(self, http_request):
+ """Retrieves the list of authorized scopes from the OAuth2 provider.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ revoke request.
+ """
+ self._do_retrieve_scopes(http_request, self.access_token)
+
+ def _do_retrieve_scopes(self, http_request, token):
+ """Retrieves the list of authorized scopes from the OAuth2 provider.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ refresh request.
+ token: A string used as the token to identify the credentials to
+ the provider.
+
+ Raises:
+ Error: When refresh fails, indicating the the access token is
+ invalid.
+ """
+ logger.info('Refreshing scopes')
+ query_params = {'access_token': token, 'fields': 'scope'}
+ token_info_uri = _update_query_params(self.token_info_uri,
+ query_params)
+ resp, content = http_request(token_info_uri)
+ content = _helpers._from_bytes(content)
+ if resp.status == http_client.OK:
+ d = json.loads(content)
+ self.scopes = set(util.string_to_scopes(d.get('scope', '')))
+ else:
+ error_msg = 'Invalid response {0}.'.format(resp.status)
+ try:
+ d = json.loads(content)
+ if 'error_description' in d:
+ error_msg = d['error_description']
+ except (TypeError, ValueError):
+ pass
+ raise Error(error_msg)
+
+
+class AccessTokenCredentials(OAuth2Credentials):
+ """Credentials object for OAuth 2.0.
+
+ Credentials can be applied to an httplib2.Http object using the
+ authorize() method, which then signs each request from that object
+ with the OAuth 2.0 access token. This set of credentials is for the
+ use case where you have acquired an OAuth 2.0 access_token from
+ another place such as a JavaScript client or another web
+ application, and wish to use it from Python. Because only the
+ access_token is present it can not be refreshed and will in time
+ expire.
+
+ AccessTokenCredentials objects may be safely pickled and unpickled.
+
+ Usage::
+
+ credentials = AccessTokenCredentials('<an access token>',
+ 'my-user-agent/1.0')
+ http = httplib2.Http()
+ http = credentials.authorize(http)
+
+ Raises:
+ AccessTokenCredentialsExpired: raised when the access_token expires or
+ is revoked.
+ """
+
+ def __init__(self, access_token, user_agent, revoke_uri=None):
+ """Create an instance of OAuth2Credentials
+
+ This is one of the few types if Credentials that you should contrust,
+ Credentials objects are usually instantiated by a Flow.
+
+ Args:
+ access_token: string, access token.
+ user_agent: string, The HTTP User-Agent to provide for this
+ application.
+ revoke_uri: string, URI for revoke endpoint. Defaults to None; a
+ token can't be revoked if this is None.
+ """
+ super(AccessTokenCredentials, self).__init__(
+ access_token,
+ None,
+ None,
+ None,
+ None,
+ None,
+ user_agent,
+ revoke_uri=revoke_uri)
+
+ @classmethod
+ def from_json(cls, json_data):
+ data = json.loads(_helpers._from_bytes(json_data))
+ retval = AccessTokenCredentials(
+ data['access_token'],
+ data['user_agent'])
+ return retval
+
+ def _refresh(self, http_request):
+ raise AccessTokenCredentialsError(
+ 'The access_token is expired or invalid and can\'t be refreshed.')
+
+ def _revoke(self, http_request):
+ """Revokes the access_token and deletes the store if available.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ revoke request.
+ """
+ self._do_revoke(http_request, self.access_token)
+
+
+def _detect_gce_environment():
+ """Determine if the current environment is Compute Engine.
+
+ Returns:
+ Boolean indicating whether or not the current environment is Google
+ Compute Engine.
+ """
+ # NOTE: The explicit ``timeout`` is a workaround. The underlying
+ # issue is that resolving an unknown host on some networks will take
+ # 20-30 seconds; making this timeout short fixes the issue, but
+ # could lead to false negatives in the event that we are on GCE, but
+ # the metadata resolution was particularly slow. The latter case is
+ # "unlikely".
+ connection = six.moves.http_client.HTTPConnection(
+ _GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT)
+
+ try:
+ headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
+ connection.request('GET', '/', headers=headers)
+ response = connection.getresponse()
+ if response.status == http_client.OK:
+ return (response.getheader(_METADATA_FLAVOR_HEADER) ==
+ _DESIRED_METADATA_FLAVOR)
+ except socket.error: # socket.timeout or socket.error(64, 'Host is down')
+ logger.info('Timeout attempting to reach GCE metadata service.')
+ return False
+ finally:
+ connection.close()
+
+
+def _in_gae_environment():
+ """Detects if the code is running in the App Engine environment.
+
+ Returns:
+ True if running in the GAE environment, False otherwise.
+ """
+ if SETTINGS.env_name is not None:
+ return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL')
+
+ try:
+ import google.appengine # noqa: unused import
+ except ImportError:
+ pass
+ else:
+ server_software = os.environ.get(_SERVER_SOFTWARE, '')
+ if server_software.startswith('Google App Engine/'):
+ SETTINGS.env_name = 'GAE_PRODUCTION'
+ return True
+ elif server_software.startswith('Development/'):
+ SETTINGS.env_name = 'GAE_LOCAL'
+ return True
+
+ return False
+
+
+def _in_gce_environment():
+ """Detect if the code is running in the Compute Engine environment.
+
+ Returns:
+ True if running in the GCE environment, False otherwise.
+ """
+ if SETTINGS.env_name is not None:
+ return SETTINGS.env_name == 'GCE_PRODUCTION'
+
+ if NO_GCE_CHECK != 'True' and _detect_gce_environment():
+ SETTINGS.env_name = 'GCE_PRODUCTION'
+ return True
+ return False
+
+
+class GoogleCredentials(OAuth2Credentials):
+ """Application Default Credentials for use in calling Google APIs.
+
+ The Application Default Credentials are being constructed as a function of
+ the environment where the code is being run.
+ More details can be found on this page:
+ https://developers.google.com/accounts/docs/application-default-credentials
+
+ Here is an example of how to use the Application Default Credentials for a
+ service that requires authentication::
+
+ from googleapiclient.discovery import build
+ from oauth2client.client import GoogleCredentials
+
+ credentials = GoogleCredentials.get_application_default()
+ service = build('compute', 'v1', credentials=credentials)
+
+ PROJECT = 'bamboo-machine-422'
+ ZONE = 'us-central1-a'
+ request = service.instances().list(project=PROJECT, zone=ZONE)
+ response = request.execute()
+
+ print(response)
+ """
+
+ NON_SERIALIZED_MEMBERS = (
+ frozenset(['_private_key']) |
+ OAuth2Credentials.NON_SERIALIZED_MEMBERS)
+ """Members that aren't serialized when object is converted to JSON."""
+
+ def __init__(self, access_token, client_id, client_secret, refresh_token,
+ token_expiry, token_uri, user_agent,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+ """Create an instance of GoogleCredentials.
+
+ This constructor is not usually called by the user, instead
+ GoogleCredentials objects are instantiated by
+ GoogleCredentials.from_stream() or
+ GoogleCredentials.get_application_default().
+
+ Args:
+ access_token: string, access token.
+ client_id: string, client identifier.
+ client_secret: string, client secret.
+ refresh_token: string, refresh token.
+ token_expiry: datetime, when the access_token expires.
+ token_uri: string, URI of token endpoint.
+ user_agent: string, The HTTP User-Agent to provide for this
+ application.
+ revoke_uri: string, URI for revoke endpoint. Defaults to
+ oauth2client.GOOGLE_REVOKE_URI; a token can't be
+ revoked if this is None.
+ """
+ super(GoogleCredentials, self).__init__(
+ access_token, client_id, client_secret, refresh_token,
+ token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
+
+ def create_scoped_required(self):
+ """Whether this Credentials object is scopeless.
+
+ create_scoped(scopes) method needs to be called in order to create
+ a Credentials object for API calls.
+ """
+ return False
+
+ def create_scoped(self, scopes):
+ """Create a Credentials object for the given scopes.
+
+ The Credentials type is preserved.
+ """
+ return self
+
+ @classmethod
+ def from_json(cls, json_data):
+ # TODO(issue 388): eliminate the circularity that is the reason for
+ # this non-top-level import.
+ from oauth2client import service_account
+ data = json.loads(_helpers._from_bytes(json_data))
+
+ # We handle service_account.ServiceAccountCredentials since it is a
+ # possible return type of GoogleCredentials.get_application_default()
+ if (data['_module'] == 'oauth2client.service_account' and
+ data['_class'] == 'ServiceAccountCredentials'):
+ return service_account.ServiceAccountCredentials.from_json(data)
+ elif (data['_module'] == 'oauth2client.service_account' and
+ data['_class'] == '_JWTAccessCredentials'):
+ return service_account._JWTAccessCredentials.from_json(data)
+
+ token_expiry = _parse_expiry(data.get('token_expiry'))
+ google_credentials = cls(
+ data['access_token'],
+ data['client_id'],
+ data['client_secret'],
+ data['refresh_token'],
+ token_expiry,
+ data['token_uri'],
+ data['user_agent'],
+ revoke_uri=data.get('revoke_uri', None))
+ google_credentials.invalid = data['invalid']
+ return google_credentials
+
+ @property
+ def serialization_data(self):
+ """Get the fields and values identifying the current credentials."""
+ return {
+ 'type': 'authorized_user',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'refresh_token': self.refresh_token
+ }
+
+ @staticmethod
+ def _implicit_credentials_from_gae():
+ """Attempts to get implicit credentials in Google App Engine env.
+
+ If the current environment is not detected as App Engine, returns None,
+ indicating no Google App Engine credentials can be detected from the
+ current environment.
+
+ Returns:
+ None, if not in GAE, else an appengine.AppAssertionCredentials
+ object.
+ """
+ if not _in_gae_environment():
+ return None
+
+ return _get_application_default_credential_GAE()
+
+ @staticmethod
+ def _implicit_credentials_from_gce():
+ """Attempts to get implicit credentials in Google Compute Engine env.
+
+ If the current environment is not detected as Compute Engine, returns
+ None, indicating no Google Compute Engine credentials can be detected
+ from the current environment.
+
+ Returns:
+ None, if not in GCE, else a gce.AppAssertionCredentials object.
+ """
+ if not _in_gce_environment():
+ return None
+
+ return _get_application_default_credential_GCE()
+
+ @staticmethod
+ def _implicit_credentials_from_files():
+ """Attempts to get implicit credentials from local credential files.
+
+ First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS
+ is set with a filename and then falls back to a configuration file (the
+ "well known" file) associated with the 'gcloud' command line tool.
+
+ Returns:
+ Credentials object associated with the
+ GOOGLE_APPLICATION_CREDENTIALS file or the "well known" file if
+ either exist. If neither file is define, returns None, indicating
+ no credentials from a file can detected from the current
+ environment.
+ """
+ credentials_filename = _get_environment_variable_file()
+ if not credentials_filename:
+ credentials_filename = _get_well_known_file()
+ if os.path.isfile(credentials_filename):
+ extra_help = (' (produced automatically when running'
+ ' "gcloud auth login" command)')
+ else:
+ credentials_filename = None
+ else:
+ extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS +
+ ' environment variable)')
+
+ if not credentials_filename:
+ return
+
+ # If we can read the credentials from a file, we don't need to know
+ # what environment we are in.
+ SETTINGS.env_name = DEFAULT_ENV_NAME
+
+ try:
+ return _get_application_default_credential_from_file(
+ credentials_filename)
+ except (ApplicationDefaultCredentialsError, ValueError) as error:
+ _raise_exception_for_reading_json(credentials_filename,
+ extra_help, error)
+
+ @classmethod
+ def _get_implicit_credentials(cls):
+ """Gets credentials implicitly from the environment.
+
+ Checks environment in order of precedence:
+ - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to
+ a file with stored credentials information.
+ - Stored "well known" file associated with `gcloud` command line tool.
+ - Google App Engine (production and testing)
+ - Google Compute Engine production environment.
+
+ Raises:
+ ApplicationDefaultCredentialsError: raised when the credentials
+ fail to be retrieved.
+ """
+ # Environ checks (in order).
+ environ_checkers = [
+ cls._implicit_credentials_from_files,
+ cls._implicit_credentials_from_gae,
+ cls._implicit_credentials_from_gce,
+ ]
+
+ for checker in environ_checkers:
+ credentials = checker()
+ if credentials is not None:
+ return credentials
+
+ # If no credentials, fail.
+ raise ApplicationDefaultCredentialsError(ADC_HELP_MSG)
+
+ @staticmethod
+ def get_application_default():
+ """Get the Application Default Credentials for the current environment.
+
+ Raises:
+ ApplicationDefaultCredentialsError: raised when the credentials
+ fail to be retrieved.
+ """
+ return GoogleCredentials._get_implicit_credentials()
+
+ @staticmethod
+ def from_stream(credential_filename):
+ """Create a Credentials object by reading information from a file.
+
+ It returns an object of type GoogleCredentials.
+
+ Args:
+ credential_filename: the path to the file from where the
+ credentials are to be read
+
+ Raises:
+ ApplicationDefaultCredentialsError: raised when the credentials
+ fail to be retrieved.
+ """
+ if credential_filename and os.path.isfile(credential_filename):
+ try:
+ return _get_application_default_credential_from_file(
+ credential_filename)
+ except (ApplicationDefaultCredentialsError, ValueError) as error:
+ extra_help = (' (provided as parameter to the '
+ 'from_stream() method)')
+ _raise_exception_for_reading_json(credential_filename,
+ extra_help,
+ error)
+ else:
+ raise ApplicationDefaultCredentialsError(
+ 'The parameter passed to the from_stream() '
+ 'method should point to a file.')
+
+
+def _save_private_file(filename, json_contents):
+ """Saves a file with read-write permissions on for the owner.
+
+ Args:
+ filename: String. Absolute path to file.
+ json_contents: JSON serializable object to be saved.
+ """
+ temp_filename = tempfile.mktemp()
+ file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600)
+ with os.fdopen(file_desc, 'w') as file_handle:
+ json.dump(json_contents, file_handle, sort_keys=True,
+ indent=2, separators=(',', ': '))
+ shutil.move(temp_filename, filename)
+
+
+def save_to_well_known_file(credentials, well_known_file=None):
+ """Save the provided GoogleCredentials to the well known file.
+
+ Args:
+ credentials: the credentials to be saved to the well known file;
+ it should be an instance of GoogleCredentials
+ well_known_file: the name of the file where the credentials are to be
+ saved; this parameter is supposed to be used for
+ testing only
+ """
+ # TODO(orestica): move this method to tools.py
+ # once the argparse import gets fixed (it is not present in Python 2.6)
+
+ if well_known_file is None:
+ well_known_file = _get_well_known_file()
+
+ config_dir = os.path.dirname(well_known_file)
+ if not os.path.isdir(config_dir):
+ raise OSError(
+ 'Config directory does not exist: {0}'.format(config_dir))
+
+ credentials_data = credentials.serialization_data
+ _save_private_file(well_known_file, credentials_data)
+
+
+def _get_environment_variable_file():
+ application_default_credential_filename = (
+ os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, None))
+
+ if application_default_credential_filename:
+ if os.path.isfile(application_default_credential_filename):
+ return application_default_credential_filename
+ else:
+ raise ApplicationDefaultCredentialsError(
+ 'File ' + application_default_credential_filename +
+ ' (pointed by ' +
+ GOOGLE_APPLICATION_CREDENTIALS +
+ ' environment variable) does not exist!')
+
+
+def _get_well_known_file():
+ """Get the well known file produced by command 'gcloud auth login'."""
+ # TODO(orestica): Revisit this method once gcloud provides a better way
+ # of pinpointing the exact location of the file.
+ default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR)
+ if default_config_dir is None:
+ if os.name == 'nt':
+ try:
+ default_config_dir = os.path.join(os.environ['APPDATA'],
+ _CLOUDSDK_CONFIG_DIRECTORY)
+ except KeyError:
+ # This should never happen unless someone is really
+ # messing with things.
+ drive = os.environ.get('SystemDrive', 'C:')
+ default_config_dir = os.path.join(drive, '\\',
+ _CLOUDSDK_CONFIG_DIRECTORY)
+ else:
+ default_config_dir = os.path.join(os.path.expanduser('~'),
+ '.config',
+ _CLOUDSDK_CONFIG_DIRECTORY)
+
+ return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE)
+
+
+def _get_application_default_credential_from_file(filename):
+ """Build the Application Default Credentials from file."""
+ # read the credentials from the file
+ with open(filename) as file_obj:
+ client_credentials = json.load(file_obj)
+
+ credentials_type = client_credentials.get('type')
+ if credentials_type == AUTHORIZED_USER:
+ required_fields = set(['client_id', 'client_secret', 'refresh_token'])
+ elif credentials_type == SERVICE_ACCOUNT:
+ required_fields = set(['client_id', 'client_email', 'private_key_id',
+ 'private_key'])
+ else:
+ raise ApplicationDefaultCredentialsError(
+ "'type' field should be defined (and have one of the '" +
+ AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
+
+ missing_fields = required_fields.difference(client_credentials.keys())
+
+ if missing_fields:
+ _raise_exception_for_missing_fields(missing_fields)
+
+ if client_credentials['type'] == AUTHORIZED_USER:
+ return GoogleCredentials(
+ access_token=None,
+ client_id=client_credentials['client_id'],
+ client_secret=client_credentials['client_secret'],
+ refresh_token=client_credentials['refresh_token'],
+ token_expiry=None,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ user_agent='Python client library')
+ else: # client_credentials['type'] == SERVICE_ACCOUNT
+ from oauth2client import service_account
+ return service_account._JWTAccessCredentials.from_json_keyfile_dict(
+ client_credentials)
+
+
+def _raise_exception_for_missing_fields(missing_fields):
+ raise ApplicationDefaultCredentialsError(
+ 'The following field(s) must be defined: ' + ', '.join(missing_fields))
+
+
+def _raise_exception_for_reading_json(credential_file,
+ extra_help,
+ error):
+ raise ApplicationDefaultCredentialsError(
+ 'An error was encountered while reading json file: ' +
+ credential_file + extra_help + ': ' + str(error))
+
+
+def _get_application_default_credential_GAE():
+ from oauth2client.contrib.appengine import AppAssertionCredentials
+
+ return AppAssertionCredentials([])
+
+
+def _get_application_default_credential_GCE():
+ from oauth2client.contrib.gce import AppAssertionCredentials
+
+ return AppAssertionCredentials()
+
+
+class AssertionCredentials(GoogleCredentials):
+ """Abstract Credentials object used for OAuth 2.0 assertion grants.
+
+ This credential does not require a flow to instantiate because it
+ represents a two legged flow, and therefore has all of the required
+ information to generate and refresh its own access tokens. It must
+ be subclassed to generate the appropriate assertion string.
+
+ AssertionCredentials objects may be safely pickled and unpickled.
+ """
+
+ @util.positional(2)
+ def __init__(self, assertion_type, user_agent=None,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+ **unused_kwargs):
+ """Constructor for AssertionFlowCredentials.
+
+ Args:
+ assertion_type: string, assertion type that will be declared to the
+ auth server
+ user_agent: string, The HTTP User-Agent to provide for this
+ application.
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ revoke_uri: string, URI for revoke endpoint.
+ """
+ super(AssertionCredentials, self).__init__(
+ None,
+ None,
+ None,
+ None,
+ None,
+ token_uri,
+ user_agent,
+ revoke_uri=revoke_uri)
+ self.assertion_type = assertion_type
+
+ def _generate_refresh_request_body(self):
+ assertion = self._generate_assertion()
+
+ body = urllib.parse.urlencode({
+ 'assertion': assertion,
+ 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+ })
+
+ return body
+
+ def _generate_assertion(self):
+ """Generate assertion string to be used in the access token request."""
+ raise NotImplementedError
+
+ def _revoke(self, http_request):
+ """Revokes the access_token and deletes the store if available.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ revoke request.
+ """
+ self._do_revoke(http_request, self.access_token)
+
+ def sign_blob(self, blob):
+ """Cryptographically sign a blob (of bytes).
+
+ Args:
+ blob: bytes, Message to be signed.
+
+ Returns:
+ tuple, A pair of the private key ID used to sign the blob and
+ the signed contents.
+ """
+ raise NotImplementedError('This method is abstract.')
+
+
+def _require_crypto_or_die():
+ """Ensure we have a crypto library, or throw CryptoUnavailableError.
+
+ The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
+ to be available in order to function, but these are optional
+ dependencies.
+ """
+ if not HAS_CRYPTO:
+ raise CryptoUnavailableError('No crypto library available')
+
+
+@util.positional(2)
+def verify_id_token(id_token, audience, http=None,
+ cert_uri=ID_TOKEN_VERIFICATION_CERTS):
+ """Verifies a signed JWT id_token.
+
+ This function requires PyOpenSSL and because of that it does not work on
+ App Engine.
+
+ Args:
+ id_token: string, A Signed JWT.
+ audience: string, The audience 'aud' that the token should be for.
+ http: httplib2.Http, instance to use to make the HTTP request. Callers
+ should supply an instance that has caching enabled.
+ cert_uri: string, URI of the certificates in JSON format to
+ verify the JWT against.
+
+ Returns:
+ The deserialized JSON in the JWT.
+
+ Raises:
+ oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
+ CryptoUnavailableError: if no crypto library is available.
+ """
+ _require_crypto_or_die()
+ if http is None:
+ http = transport.get_cached_http()
+
+ resp, content = http.request(cert_uri)
+ if resp.status == http_client.OK:
+ certs = json.loads(_helpers._from_bytes(content))
+ return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
+ else:
+ raise VerifyJwtTokenError('Status code: {0}'.format(resp.status))
+
+
+def _extract_id_token(id_token):
+ """Extract the JSON payload from a JWT.
+
+ Does the extraction w/o checking the signature.
+
+ Args:
+ id_token: string or bytestring, OAuth 2.0 id_token.
+
+ Returns:
+ object, The deserialized JSON payload.
+ """
+ if type(id_token) == bytes:
+ segments = id_token.split(b'.')
+ else:
+ segments = id_token.split(u'.')
+
+ if len(segments) != 3:
+ raise VerifyJwtTokenError(
+ 'Wrong number of segments in token: {0}'.format(id_token))
+
+ return json.loads(
+ _helpers._from_bytes(_helpers._urlsafe_b64decode(segments[1])))
+
+
+def _parse_exchange_token_response(content):
+ """Parses response of an exchange token request.
+
+ Most providers return JSON but some (e.g. Facebook) return a
+ url-encoded string.
+
+ Args:
+ content: The body of a response
+
+ Returns:
+ Content as a dictionary object. Note that the dict could be empty,
+ i.e. {}. That basically indicates a failure.
+ """
+ resp = {}
+ content = _helpers._from_bytes(content)
+ try:
+ resp = json.loads(content)
+ except Exception:
+ # different JSON libs raise different exceptions,
+ # so we just do a catch-all here
+ resp = dict(urllib.parse.parse_qsl(content))
+
+ # some providers respond with 'expires', others with 'expires_in'
+ if resp and 'expires' in resp:
+ resp['expires_in'] = resp.pop('expires')
+
+ return resp
+
+
+@util.positional(4)
+def credentials_from_code(client_id, client_secret, scope, code,
+ redirect_uri='postmessage', http=None,
+ user_agent=None,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ auth_uri=oauth2client.GOOGLE_AUTH_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+ device_uri=oauth2client.GOOGLE_DEVICE_URI,
+ token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI):
+ """Exchanges an authorization code for an OAuth2Credentials object.
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string, client secret.
+ scope: string or iterable of strings, scope(s) to request.
+ code: string, An authorization code, most likely passed down from
+ the client
+ redirect_uri: string, this is generally set to 'postmessage' to match
+ the redirect_uri that the client specified
+ http: httplib2.Http, optional http instance to use to do the fetch
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ auth_uri: string, URI for authorization endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider
+ can be used.
+ revoke_uri: string, URI for revoke endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider
+ can be used.
+ device_uri: string, URI for device authorization endpoint. For
+ convenience defaults to Google's endpoints but any OAuth
+ 2.0 provider can be used.
+
+ Returns:
+ An OAuth2Credentials object.
+
+ Raises:
+ FlowExchangeError if the authorization code cannot be exchanged for an
+ access token
+ """
+ flow = OAuth2WebServerFlow(client_id, client_secret, scope,
+ redirect_uri=redirect_uri,
+ user_agent=user_agent, auth_uri=auth_uri,
+ token_uri=token_uri, revoke_uri=revoke_uri,
+ device_uri=device_uri,
+ token_info_uri=token_info_uri)
+
+ credentials = flow.step2_exchange(code, http=http)
+ return credentials
+
+
+@util.positional(3)
+def credentials_from_clientsecrets_and_code(filename, scope, code,
+ message=None,
+ redirect_uri='postmessage',
+ http=None,
+ cache=None,
+ device_uri=None):
+ """Returns OAuth2Credentials from a clientsecrets file and an auth code.
+
+ Will create the right kind of Flow based on the contents of the
+ clientsecrets file or will raise InvalidClientSecretsError for unknown
+ types of Flows.
+
+ Args:
+ filename: string, File name of clientsecrets.
+ scope: string or iterable of strings, scope(s) to request.
+ code: string, An authorization code, most likely passed down from
+ the client
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. If message is
+ provided then sys.exit will be called in the case of an error.
+ If message in not provided then
+ clientsecrets.InvalidClientSecretsError will be raised.
+ redirect_uri: string, this is generally set to 'postmessage' to match
+ the redirect_uri that the client specified
+ http: httplib2.Http, optional http instance to use to do the fetch
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+ device_uri: string, OAuth 2.0 device authorization endpoint
+
+ Returns:
+ An OAuth2Credentials object.
+
+ Raises:
+ FlowExchangeError: if the authorization code cannot be exchanged for an
+ access token
+ UnknownClientSecretsFlowError: if the file describes an unknown kind
+ of Flow.
+ clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
+ invalid.
+ """
+ flow = flow_from_clientsecrets(filename, scope, message=message,
+ cache=cache, redirect_uri=redirect_uri,
+ device_uri=device_uri)
+ credentials = flow.step2_exchange(code, http=http)
+ return credentials
+
+
+class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
+ 'device_code', 'user_code', 'interval', 'verification_url',
+ 'user_code_expiry'))):
+ """Intermediate information the OAuth2 for devices flow."""
+
+ @classmethod
+ def FromResponse(cls, response):
+ """Create a DeviceFlowInfo from a server response.
+
+ The response should be a dict containing entries as described here:
+
+ http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1
+ """
+ # device_code, user_code, and verification_url are required.
+ kwargs = {
+ 'device_code': response['device_code'],
+ 'user_code': response['user_code'],
+ }
+ # The response may list the verification address as either
+ # verification_url or verification_uri, so we check for both.
+ verification_url = response.get(
+ 'verification_url', response.get('verification_uri'))
+ if verification_url is None:
+ raise OAuth2DeviceCodeError(
+ 'No verification_url provided in server response')
+ kwargs['verification_url'] = verification_url
+ # expires_in and interval are optional.
+ kwargs.update({
+ 'interval': response.get('interval'),
+ 'user_code_expiry': None,
+ })
+ if 'expires_in' in response:
+ kwargs['user_code_expiry'] = (
+ _UTCNOW() +
+ datetime.timedelta(seconds=int(response['expires_in'])))
+ return cls(**kwargs)
+
+
+def _oauth2_web_server_flow_params(kwargs):
+ """Configures redirect URI parameters for OAuth2WebServerFlow."""
+ params = {
+ 'access_type': 'offline',
+ 'response_type': 'code',
+ }
+
+ params.update(kwargs)
+
+ # Check for the presence of the deprecated approval_prompt param and
+ # warn appropriately.
+ approval_prompt = params.get('approval_prompt')
+ if approval_prompt is not None:
+ logger.warning(
+ 'The approval_prompt parameter for OAuth2WebServerFlow is '
+ 'deprecated. Please use the prompt parameter instead.')
+
+ if approval_prompt == 'force':
+ logger.warning(
+ 'approval_prompt="force" has been adjusted to '
+ 'prompt="consent"')
+ params['prompt'] = 'consent'
+ del params['approval_prompt']
+
+ return params
+
+
+class OAuth2WebServerFlow(Flow):
+ """Does the Web Server Flow for OAuth 2.0.
+
+ OAuth2WebServerFlow objects may be safely pickled and unpickled.
+ """
+
+ @util.positional(4)
+ def __init__(self, client_id,
+ client_secret=None,
+ scope=None,
+ redirect_uri=None,
+ user_agent=None,
+ auth_uri=oauth2client.GOOGLE_AUTH_URI,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+ login_hint=None,
+ device_uri=oauth2client.GOOGLE_DEVICE_URI,
+ token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
+ authorization_header=None,
+ **kwargs):
+ """Constructor for OAuth2WebServerFlow.
+
+ The kwargs argument is used to set extra query parameters on the
+ auth_uri. For example, the access_type and prompt
+ query parameters can be set via kwargs.
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string client secret.
+ scope: string or iterable of strings, scope(s) of the credentials
+ being requested.
+ redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
+ for a non-web-based application, or a URI that
+ handles the callback from the authorization server.
+ user_agent: string, HTTP User-Agent to provide for this
+ application.
+ auth_uri: string, URI for authorization endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider
+ can be used.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0
+ provider can be used.
+ revoke_uri: string, URI for revoke endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0
+ provider can be used.
+ login_hint: string, Either an email address or domain. Passing this
+ hint will either pre-fill the email box on the sign-in
+ form or select the proper multi-login session, thereby
+ simplifying the login flow.
+ device_uri: string, URI for device authorization endpoint. For
+ convenience defaults to Google's endpoints but any
+ OAuth 2.0 provider can be used.
+ authorization_header: string, For use with OAuth 2.0 providers that
+ require a client to authenticate using a
+ header value instead of passing client_secret
+ in the POST body.
+ **kwargs: dict, The keyword arguments are all optional and required
+ parameters for the OAuth calls.
+ """
+ # scope is a required argument, but to preserve backwards-compatibility
+ # we don't want to rearrange the positional arguments
+ if scope is None:
+ raise TypeError("The value of scope must not be None")
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.scope = util.scopes_to_string(scope)
+ self.redirect_uri = redirect_uri
+ self.login_hint = login_hint
+ self.user_agent = user_agent
+ self.auth_uri = auth_uri
+ self.token_uri = token_uri
+ self.revoke_uri = revoke_uri
+ self.device_uri = device_uri
+ self.token_info_uri = token_info_uri
+ self.authorization_header = authorization_header
+ self.params = _oauth2_web_server_flow_params(kwargs)
+
+ @util.positional(1)
+ def step1_get_authorize_url(self, redirect_uri=None, state=None):
+ """Returns a URI to redirect to the provider.
+
+ Args:
+ redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
+ for a non-web-based application, or a URI that
+ handles the callback from the authorization server.
+ This parameter is deprecated, please move to passing
+ the redirect_uri in via the constructor.
+ state: string, Opaque state string which is passed through the
+ OAuth2 flow and returned to the client as a query parameter
+ in the callback.
+
+ Returns:
+ A URI as a string to redirect the user to begin the authorization
+ flow.
+ """
+ if redirect_uri is not None:
+ logger.warning((
+ 'The redirect_uri parameter for '
+ 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. '
+ 'Please move to passing the redirect_uri in via the '
+ 'constructor.'))
+ self.redirect_uri = redirect_uri
+
+ if self.redirect_uri is None:
+ raise ValueError('The value of redirect_uri must not be None.')
+
+ query_params = {
+ 'client_id': self.client_id,
+ 'redirect_uri': self.redirect_uri,
+ 'scope': self.scope,
+ }
+ if state is not None:
+ query_params['state'] = state
+ if self.login_hint is not None:
+ query_params['login_hint'] = self.login_hint
+ query_params.update(self.params)
+ return _update_query_params(self.auth_uri, query_params)
+
+ @util.positional(1)
+ def step1_get_device_and_user_codes(self, http=None):
+ """Returns a user code and the verification URL where to enter it
+
+ Returns:
+ A user code as a string for the user to authorize the application
+ An URL as a string where the user has to enter the code
+ """
+ if self.device_uri is None:
+ raise ValueError('The value of device_uri must not be None.')
+
+ body = urllib.parse.urlencode({
+ 'client_id': self.client_id,
+ 'scope': self.scope,
+ })
+ headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+
+ if self.user_agent is not None:
+ headers['user-agent'] = self.user_agent
+
+ if http is None:
+ http = transport.get_http_object()
+
+ resp, content = http.request(self.device_uri, method='POST', body=body,
+ headers=headers)
+ content = _helpers._from_bytes(content)
+ if resp.status == http_client.OK:
+ try:
+ flow_info = json.loads(content)
+ except ValueError as exc:
+ raise OAuth2DeviceCodeError(
+ 'Could not parse server response as JSON: "{0}", '
+ 'error: "{1}"'.format(content, exc))
+ return DeviceFlowInfo.FromResponse(flow_info)
+ else:
+ error_msg = 'Invalid response {0}.'.format(resp.status)
+ try:
+ error_dict = json.loads(content)
+ if 'error' in error_dict:
+ error_msg += ' Error: {0}'.format(error_dict['error'])
+ except ValueError:
+ # Couldn't decode a JSON response, stick with the
+ # default message.
+ pass
+ raise OAuth2DeviceCodeError(error_msg)
+
+ @util.positional(2)
+ def step2_exchange(self, code=None, http=None, device_flow_info=None):
+ """Exchanges a code for OAuth2Credentials.
+
+ Args:
+ code: string, a dict-like object, or None. For a non-device
+ flow, this is either the response code as a string, or a
+ dictionary of query parameters to the redirect_uri. For a
+ device flow, this should be None.
+ http: httplib2.Http, optional http instance to use when fetching
+ credentials.
+ device_flow_info: DeviceFlowInfo, return value from step1 in the
+ case of a device flow.
+
+ Returns:
+ An OAuth2Credentials object that can be used to authorize requests.
+
+ Raises:
+ FlowExchangeError: if a problem occurred exchanging the code for a
+ refresh_token.
+ ValueError: if code and device_flow_info are both provided or both
+ missing.
+ """
+ if code is None and device_flow_info is None:
+ raise ValueError('No code or device_flow_info provided.')
+ if code is not None and device_flow_info is not None:
+ raise ValueError('Cannot provide both code and device_flow_info.')
+
+ if code is None:
+ code = device_flow_info.device_code
+ elif not isinstance(code, (six.string_types, six.binary_type)):
+ if 'code' not in code:
+ raise FlowExchangeError(code.get(
+ 'error', 'No code was supplied in the query parameters.'))
+ code = code['code']
+
+ post_data = {
+ 'client_id': self.client_id,
+ 'code': code,
+ 'scope': self.scope,
+ }
+ if self.client_secret is not None:
+ post_data['client_secret'] = self.client_secret
+ if device_flow_info is not None:
+ post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
+ else:
+ post_data['grant_type'] = 'authorization_code'
+ post_data['redirect_uri'] = self.redirect_uri
+ body = urllib.parse.urlencode(post_data)
+ headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+ if self.authorization_header is not None:
+ headers['Authorization'] = self.authorization_header
+ if self.user_agent is not None:
+ headers['user-agent'] = self.user_agent
+
+ if http is None:
+ http = transport.get_http_object()
+
+ resp, content = http.request(self.token_uri, method='POST', body=body,
+ headers=headers)
+ d = _parse_exchange_token_response(content)
+ if resp.status == http_client.OK and 'access_token' in d:
+ access_token = d['access_token']
+ refresh_token = d.get('refresh_token', None)
+ if not refresh_token:
+ logger.info(
+ 'Received token response with no refresh_token. Consider '
+ "reauthenticating with prompt='consent'.")
+ token_expiry = None
+ if 'expires_in' in d:
+ delta = datetime.timedelta(seconds=int(d['expires_in']))
+ token_expiry = delta + _UTCNOW()
+
+ extracted_id_token = None
+ if 'id_token' in d:
+ extracted_id_token = _extract_id_token(d['id_token'])
+
+ logger.info('Successfully retrieved access token')
+ return OAuth2Credentials(
+ access_token, self.client_id, self.client_secret,
+ refresh_token, token_expiry, self.token_uri, self.user_agent,
+ revoke_uri=self.revoke_uri, id_token=extracted_id_token,
+ token_response=d, scopes=self.scope,
+ token_info_uri=self.token_info_uri)
+ else:
+ logger.info('Failed to retrieve access token: %s', content)
+ if 'error' in d:
+ # you never know what those providers got to say
+ error_msg = (str(d['error']) +
+ str(d.get('error_description', '')))
+ else:
+ error_msg = 'Invalid response: {0}.'.format(str(resp.status))
+ raise FlowExchangeError(error_msg)
+
+
+@util.positional(2)
+def flow_from_clientsecrets(filename, scope, redirect_uri=None,
+ message=None, cache=None, login_hint=None,
+ device_uri=None):
+ """Create a Flow from a clientsecrets file.
+
+ Will create the right kind of Flow based on the contents of the
+ clientsecrets file or will raise InvalidClientSecretsError for unknown
+ types of Flows.
+
+ Args:
+ filename: string, File name of client secrets.
+ scope: string or iterable of strings, scope(s) to request.
+ redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+ a non-web-based application, or a URI that handles the
+ callback from the authorization server.
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. If message is
+ provided then sys.exit will be called in the case of an error.
+ If message in not provided then
+ clientsecrets.InvalidClientSecretsError will be raised.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+ login_hint: string, Either an email address or domain. Passing this
+ hint will either pre-fill the email box on the sign-in form
+ or select the proper multi-login session, thereby
+ simplifying the login flow.
+ device_uri: string, URI for device authorization endpoint. For
+ convenience defaults to Google's endpoints but any
+ OAuth 2.0 provider can be used.
+
+ Returns:
+ A Flow object.
+
+ Raises:
+ UnknownClientSecretsFlowError: if the file describes an unknown kind of
+ Flow.
+ clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
+ invalid.
+ """
+ try:
+ client_type, client_info = clientsecrets.loadfile(filename,
+ cache=cache)
+ if client_type in (clientsecrets.TYPE_WEB,
+ clientsecrets.TYPE_INSTALLED):
+ constructor_kwargs = {
+ 'redirect_uri': redirect_uri,
+ 'auth_uri': client_info['auth_uri'],
+ 'token_uri': client_info['token_uri'],
+ 'login_hint': login_hint,
+ }
+ revoke_uri = client_info.get('revoke_uri')
+ if revoke_uri is not None:
+ constructor_kwargs['revoke_uri'] = revoke_uri
+ if device_uri is not None:
+ constructor_kwargs['device_uri'] = device_uri
+ return OAuth2WebServerFlow(
+ client_info['client_id'], client_info['client_secret'],
+ scope, **constructor_kwargs)
+
+ except clientsecrets.InvalidClientSecretsError as e:
+ if message is not None:
+ if e.args:
+ message = ('The client secrets were invalid: '
+ '\n{0}\n{1}'.format(e, message))
+ sys.exit(message)
+ else:
+ raise
+ else:
+ raise UnknownClientSecretsFlowError(
+ 'This OAuth 2.0 flow is unsupported: {0!r}'.format(client_type))
diff --git a/oauth2client/clientsecrets.py b/oauth2client/clientsecrets.py
new file mode 100644
index 0000000..4b43e66
--- /dev/null
+++ b/oauth2client/clientsecrets.py
@@ -0,0 +1,174 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for reading OAuth 2.0 client secret files.
+
+A client_secrets.json file contains all the information needed to interact with
+an OAuth 2.0 protected service.
+"""
+
+import json
+
+import six
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+# Properties that make a client_secrets.json file valid.
+TYPE_WEB = 'web'
+TYPE_INSTALLED = 'installed'
+
+VALID_CLIENT = {
+ TYPE_WEB: {
+ 'required': [
+ 'client_id',
+ 'client_secret',
+ 'redirect_uris',
+ 'auth_uri',
+ 'token_uri',
+ ],
+ 'string': [
+ 'client_id',
+ 'client_secret',
+ ],
+ },
+ TYPE_INSTALLED: {
+ 'required': [
+ 'client_id',
+ 'client_secret',
+ 'redirect_uris',
+ 'auth_uri',
+ 'token_uri',
+ ],
+ 'string': [
+ 'client_id',
+ 'client_secret',
+ ],
+ },
+}
+
+
+class Error(Exception):
+ """Base error for this module."""
+
+
+class InvalidClientSecretsError(Error):
+ """Format of ClientSecrets file is invalid."""
+
+
+def _validate_clientsecrets(clientsecrets_dict):
+ """Validate parsed client secrets from a file.
+
+ Args:
+ clientsecrets_dict: dict, a dictionary holding the client secrets.
+
+ Returns:
+ tuple, a string of the client type and the information parsed
+ from the file.
+ """
+ _INVALID_FILE_FORMAT_MSG = (
+ 'Invalid file format. See '
+ 'https://developers.google.com/api-client-library/'
+ 'python/guide/aaa_client_secrets')
+
+ if clientsecrets_dict is None:
+ raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
+ try:
+ (client_type, client_info), = clientsecrets_dict.items()
+ except (ValueError, AttributeError):
+ raise InvalidClientSecretsError(
+ _INVALID_FILE_FORMAT_MSG + ' '
+ 'Expected a JSON object with a single property for a "web" or '
+ '"installed" application')
+
+ if client_type not in VALID_CLIENT:
+ raise InvalidClientSecretsError(
+ 'Unknown client type: {0}.'.format(client_type))
+
+ for prop_name in VALID_CLIENT[client_type]['required']:
+ if prop_name not in client_info:
+ raise InvalidClientSecretsError(
+ 'Missing property "{0}" in a client type of "{1}".'.format(
+ prop_name, client_type))
+ for prop_name in VALID_CLIENT[client_type]['string']:
+ if client_info[prop_name].startswith('[['):
+ raise InvalidClientSecretsError(
+ 'Property "{0}" is not configured.'.format(prop_name))
+ return client_type, client_info
+
+
+def load(fp):
+ obj = json.load(fp)
+ return _validate_clientsecrets(obj)
+
+
+def loads(s):
+ obj = json.loads(s)
+ return _validate_clientsecrets(obj)
+
+
+def _loadfile(filename):
+ try:
+ with open(filename, 'r') as fp:
+ obj = json.load(fp)
+ except IOError as exc:
+ raise InvalidClientSecretsError('Error opening file', exc.filename,
+ exc.strerror, exc.errno)
+ return _validate_clientsecrets(obj)
+
+
+def loadfile(filename, cache=None):
+ """Loading of client_secrets JSON file, optionally backed by a cache.
+
+ Typical cache storage would be App Engine memcache service,
+ but you can pass in any other cache client that implements
+ these methods:
+
+ * ``get(key, namespace=ns)``
+ * ``set(key, value, namespace=ns)``
+
+ Usage::
+
+ # without caching
+ client_type, client_info = loadfile('secrets.json')
+ # using App Engine memcache service
+ from google.appengine.api import memcache
+ client_type, client_info = loadfile('secrets.json', cache=memcache)
+
+ Args:
+ filename: string, Path to a client_secrets.json file on a filesystem.
+ cache: An optional cache service client that implements get() and set()
+ methods. If not specified, the file is always being loaded from
+ a filesystem.
+
+ Raises:
+ InvalidClientSecretsError: In case of a validation error or some
+ I/O failure. Can happen only on cache miss.
+
+ Returns:
+ (client_type, client_info) tuple, as _loadfile() normally would.
+ JSON contents is validated only during first load. Cache hits are not
+ validated.
+ """
+ _SECRET_NAMESPACE = 'oauth2client:secrets#ns'
+
+ if not cache:
+ return _loadfile(filename)
+
+ obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
+ if obj is None:
+ client_type, client_info = _loadfile(filename)
+ obj = {client_type: client_info}
+ cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
+
+ return next(six.iteritems(obj))
diff --git a/oauth2client/contrib/__init__.py b/oauth2client/contrib/__init__.py
new file mode 100644
index 0000000..ecfd06c
--- /dev/null
+++ b/oauth2client/contrib/__init__.py
@@ -0,0 +1,6 @@
+"""Contributed modules.
+
+Contrib contains modules that are not considered part of the core oauth2client
+library but provide additional functionality. These modules are intended to
+make it easier to use oauth2client.
+"""
diff --git a/oauth2client/contrib/_appengine_ndb.py b/oauth2client/contrib/_appengine_ndb.py
new file mode 100644
index 0000000..c863e8f
--- /dev/null
+++ b/oauth2client/contrib/_appengine_ndb.py
@@ -0,0 +1,163 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Google App Engine utilities helper.
+
+Classes that directly require App Engine's ndb library. Provided
+as a separate module in case of failure to import ndb while
+other App Engine libraries are present.
+"""
+
+import logging
+
+from google.appengine.ext import ndb
+
+from oauth2client import client
+
+
+NDB_KEY = ndb.Key
+"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
+
+NDB_MODEL = ndb.Model
+"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SiteXsrfSecretKeyNDB(ndb.Model):
+ """NDB Model for storage for the sites XSRF secret key.
+
+ Since this model uses the same kind as SiteXsrfSecretKey, it can be
+ used interchangeably. This simply provides an NDB model for interacting
+ with the same data the DB model interacts with.
+
+ There should only be one instance stored of this model, the one used
+ for the site.
+ """
+ secret = ndb.StringProperty()
+
+ @classmethod
+ def _get_kind(cls):
+ """Return the kind name for this class."""
+ return 'SiteXsrfSecretKey'
+
+
+class FlowNDBProperty(ndb.PickleProperty):
+ """App Engine NDB datastore Property for Flow.
+
+ Serves the same purpose as the DB FlowProperty, but for NDB models.
+ Since PickleProperty inherits from BlobProperty, the underlying
+ representation of the data in the datastore will be the same as in the
+ DB case.
+
+ Utility property that allows easy storage and retrieval of an
+ oauth2client.Flow
+ """
+
+ def _validate(self, value):
+ """Validates a value as a proper Flow object.
+
+ Args:
+ value: A value to be set on the property.
+
+ Raises:
+ TypeError if the value is not an instance of Flow.
+ """
+ _LOGGER.info('validate: Got type %s', type(value))
+ if value is not None and not isinstance(value, client.Flow):
+ raise TypeError(
+ 'Property {0} must be convertible to a flow '
+ 'instance; received: {1}.'.format(self._name, value))
+
+
+class CredentialsNDBProperty(ndb.BlobProperty):
+ """App Engine NDB datastore Property for Credentials.
+
+ Serves the same purpose as the DB CredentialsProperty, but for NDB
+ models. Since CredentialsProperty stores data as a blob and this
+ inherits from BlobProperty, the data in the datastore will be the same
+ as in the DB case.
+
+ Utility property that allows easy storage and retrieval of Credentials
+ and subclasses.
+ """
+
+ def _validate(self, value):
+ """Validates a value as a proper credentials object.
+
+ Args:
+ value: A value to be set on the property.
+
+ Raises:
+ TypeError if the value is not an instance of Credentials.
+ """
+ _LOGGER.info('validate: Got type %s', type(value))
+ if value is not None and not isinstance(value, client.Credentials):
+ raise TypeError(
+ 'Property {0} must be convertible to a credentials '
+ 'instance; received: {1}.'.format(self._name, value))
+
+ def _to_base_type(self, value):
+ """Converts our validated value to a JSON serialized string.
+
+ Args:
+ value: A value to be set in the datastore.
+
+ Returns:
+ A JSON serialized version of the credential, else '' if value
+ is None.
+ """
+ if value is None:
+ return ''
+ else:
+ return value.to_json()
+
+ def _from_base_type(self, value):
+ """Converts our stored JSON string back to the desired type.
+
+ Args:
+ value: A value from the datastore to be converted to the
+ desired type.
+
+ Returns:
+ A deserialized Credentials (or subclass) object, else None if
+ the value can't be parsed.
+ """
+ if not value:
+ return None
+ try:
+ # Uses the from_json method of the implied class of value
+ credentials = client.Credentials.new_from_json(value)
+ except ValueError:
+ credentials = None
+ return credentials
+
+
+class CredentialsNDBModel(ndb.Model):
+ """NDB Model for storage of OAuth 2.0 Credentials
+
+ Since this model uses the same kind as CredentialsModel and has a
+ property which can serialize and deserialize Credentials correctly, it
+ can be used interchangeably with a CredentialsModel to access, insert
+ and delete the same entities. This simply provides an NDB model for
+ interacting with the same data the DB model interacts with.
+
+ Storage of the model is keyed by the user.user_id().
+ """
+ credentials = CredentialsNDBProperty()
+
+ @classmethod
+ def _get_kind(cls):
+ """Return the kind name for this class."""
+ return 'CredentialsModel'
diff --git a/oauth2client/contrib/_fcntl_opener.py b/oauth2client/contrib/_fcntl_opener.py
new file mode 100644
index 0000000..ae6c85b
--- /dev/null
+++ b/oauth2client/contrib/_fcntl_opener.py
@@ -0,0 +1,81 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import errno
+import fcntl
+import time
+
+from oauth2client.contrib import locked_file
+
+
+class _FcntlOpener(locked_file._Opener):
+ """Open, lock, and unlock a file using fcntl.lockf."""
+
+ def open_and_lock(self, timeout, delay):
+ """Open the file and lock it.
+
+ Args:
+ timeout: float, How long to try to lock for.
+ delay: float, How long to wait between retries
+
+ Raises:
+ AlreadyLockedException: if the lock is already acquired.
+ IOError: if the open fails.
+ CredentialsFileSymbolicLinkError: if the file is a symbolic
+ link.
+ """
+ if self._locked:
+ raise locked_file.AlreadyLockedException(
+ 'File {0} is already locked'.format(self._filename))
+ start_time = time.time()
+
+ locked_file.validate_file(self._filename)
+ try:
+ self._fh = open(self._filename, self._mode)
+ except IOError as e:
+ # If we can't access with _mode, try _fallback_mode and
+ # don't lock.
+ if e.errno in (errno.EPERM, errno.EACCES):
+ self._fh = open(self._filename, self._fallback_mode)
+ return
+
+ # We opened in _mode, try to lock the file.
+ while True:
+ try:
+ fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
+ self._locked = True
+ return
+ except IOError as e:
+ # If not retrying, then just pass on the error.
+ if timeout == 0:
+ raise
+ if e.errno != errno.EACCES:
+ raise
+ # We could not acquire the lock. Try again.
+ if (time.time() - start_time) >= timeout:
+ locked_file.logger.warn('Could not lock %s in %s seconds',
+ self._filename, timeout)
+ if self._fh:
+ self._fh.close()
+ self._fh = open(self._filename, self._fallback_mode)
+ return
+ time.sleep(delay)
+
+ def unlock_and_close(self):
+ """Close and unlock the file using the fcntl.lockf primitive."""
+ if self._locked:
+ fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
+ self._locked = False
+ if self._fh:
+ self._fh.close()
diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py
new file mode 100644
index 0000000..10e6a69
--- /dev/null
+++ b/oauth2client/contrib/_metadata.py
@@ -0,0 +1,123 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Provides helper methods for talking to the Compute Engine metadata server.
+
+See https://cloud.google.com/compute/docs/metadata
+"""
+
+import datetime
+import json
+
+import httplib2
+from six.moves import http_client
+from six.moves.urllib import parse as urlparse
+
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import util
+
+
+METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
+METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
+
+
+def get(http_request, path, root=METADATA_ROOT, recursive=None):
+ """Fetch a resource from the metadata server.
+
+ Args:
+ path: A string indicating the resource to retrieve. For example,
+ 'instance/service-accounts/defualt'
+ http_request: A callable that matches the method
+ signature of httplib2.Http.request. Used to make the request to the
+ metadataserver.
+ root: A string indicating the full path to the metadata server root.
+ recursive: A boolean indicating whether to do a recursive query of
+ metadata. See
+ https://cloud.google.com/compute/docs/metadata#aggcontents
+
+ Returns:
+ A dictionary if the metadata server returns JSON, otherwise a string.
+
+ Raises:
+ httplib2.Httplib2Error if an error corrured while retrieving metadata.
+ """
+ url = urlparse.urljoin(root, path)
+ url = util._add_query_parameter(url, 'recursive', recursive)
+
+ response, content = http_request(
+ url,
+ headers=METADATA_HEADERS
+ )
+
+ if response.status == http_client.OK:
+ decoded = _helpers._from_bytes(content)
+ if response['content-type'] == 'application/json':
+ return json.loads(decoded)
+ else:
+ return decoded
+ else:
+ raise httplib2.HttpLib2Error(
+ 'Failed to retrieve {0} from the Google Compute Engine'
+ 'metadata service. Response:\n{1}'.format(url, response))
+
+
+def get_service_account_info(http_request, service_account='default'):
+ """Get information about a service account from the metadata server.
+
+ Args:
+ service_account: An email specifying the service account for which to
+ look up information. Default will be information for the "default"
+ service account of the current compute engine instance.
+ http_request: A callable that matches the method
+ signature of httplib2.Http.request. Used to make the request to the
+ metadata server.
+ Returns:
+ A dictionary with information about the specified service account,
+ for example:
+
+ {
+ 'email': '...',
+ 'scopes': ['scope', ...],
+ 'aliases': ['default', '...']
+ }
+ """
+ return get(
+ http_request,
+ 'instance/service-accounts/{0}/'.format(service_account),
+ recursive=True)
+
+
+def get_token(http_request, service_account='default'):
+ """Fetch an oauth token for the
+
+ Args:
+ service_account: An email specifying the service account this token
+ should represent. Default will be a token for the "default" service
+ account of the current compute engine instance.
+ http_request: A callable that matches the method
+ signature of httplib2.Http.request. Used to make the request to the
+ metadataserver.
+
+ Returns:
+ A tuple of (access token, token expiration), where access token is the
+ access token as a string and token expiration is a datetime object
+ that indicates when the access token will expire.
+ """
+ token_json = get(
+ http_request,
+ 'instance/service-accounts/{0}/token'.format(service_account))
+ token_expiry = client._UTCNOW() + datetime.timedelta(
+ seconds=token_json['expires_in'])
+ return token_json['access_token'], token_expiry
diff --git a/oauth2client/contrib/_win32_opener.py b/oauth2client/contrib/_win32_opener.py
new file mode 100644
index 0000000..34b4f48
--- /dev/null
+++ b/oauth2client/contrib/_win32_opener.py
@@ -0,0 +1,106 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import errno
+import time
+
+import pywintypes
+import win32con
+import win32file
+
+from oauth2client.contrib import locked_file
+
+
+class _Win32Opener(locked_file._Opener):
+ """Open, lock, and unlock a file using windows primitives."""
+
+ # Error #33:
+ # 'The process cannot access the file because another process'
+ FILE_IN_USE_ERROR = 33
+
+ # Error #158:
+ # 'The segment is already unlocked.'
+ FILE_ALREADY_UNLOCKED_ERROR = 158
+
+ def open_and_lock(self, timeout, delay):
+ """Open the file and lock it.
+
+ Args:
+ timeout: float, How long to try to lock for.
+ delay: float, How long to wait between retries
+
+ Raises:
+ AlreadyLockedException: if the lock is already acquired.
+ IOError: if the open fails.
+ CredentialsFileSymbolicLinkError: if the file is a symbolic
+ link.
+ """
+ if self._locked:
+ raise locked_file.AlreadyLockedException(
+ 'File {0} is already locked'.format(self._filename))
+ start_time = time.time()
+
+ locked_file.validate_file(self._filename)
+ try:
+ self._fh = open(self._filename, self._mode)
+ except IOError as e:
+ # If we can't access with _mode, try _fallback_mode
+ # and don't lock.
+ if e.errno == errno.EACCES:
+ self._fh = open(self._filename, self._fallback_mode)
+ return
+
+ # We opened in _mode, try to lock the file.
+ while True:
+ try:
+ hfile = win32file._get_osfhandle(self._fh.fileno())
+ win32file.LockFileEx(
+ hfile,
+ (win32con.LOCKFILE_FAIL_IMMEDIATELY |
+ win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
+ pywintypes.OVERLAPPED())
+ self._locked = True
+ return
+ except pywintypes.error as e:
+ if timeout == 0:
+ raise
+
+ # If the error is not that the file is already
+ # in use, raise.
+ if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
+ raise
+
+ # We could not acquire the lock. Try again.
+ if (time.time() - start_time) >= timeout:
+ locked_file.logger.warn('Could not lock %s in %s seconds',
+ self._filename, timeout)
+ if self._fh:
+ self._fh.close()
+ self._fh = open(self._filename, self._fallback_mode)
+ return
+ time.sleep(delay)
+
+ def unlock_and_close(self):
+ """Close and unlock the file using the win32 primitive."""
+ if self._locked:
+ try:
+ hfile = win32file._get_osfhandle(self._fh.fileno())
+ win32file.UnlockFileEx(hfile, 0, -0x10000,
+ pywintypes.OVERLAPPED())
+ except pywintypes.error as e:
+ if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
+ raise
+ self._locked = False
+ if self._fh:
+ self._fh.close()
diff --git a/oauth2client/contrib/appengine.py b/oauth2client/contrib/appengine.py
new file mode 100644
index 0000000..661105e
--- /dev/null
+++ b/oauth2client/contrib/appengine.py
@@ -0,0 +1,913 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for Google App Engine
+
+Utilities for making it easier to use OAuth 2.0 on Google App Engine.
+"""
+
+import cgi
+import json
+import logging
+import os
+import pickle
+import threading
+
+from google.appengine.api import app_identity
+from google.appengine.api import memcache
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext.webapp.util import login_required
+import httplib2
+import webapp2 as webapp
+
+import oauth2client
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client import util
+from oauth2client.contrib import xsrfutil
+
+# This is a temporary fix for a Google internal issue.
+try:
+ from oauth2client.contrib import _appengine_ndb
+except ImportError: # pragma: NO COVER
+ _appengine_ndb = None
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+logger = logging.getLogger(__name__)
+
+OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
+
+XSRF_MEMCACHE_ID = 'xsrf_secret_key'
+
+if _appengine_ndb is None: # pragma: NO COVER
+ CredentialsNDBModel = None
+ CredentialsNDBProperty = None
+ FlowNDBProperty = None
+ _NDB_KEY = None
+ _NDB_MODEL = None
+ SiteXsrfSecretKeyNDB = None
+else:
+ CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
+ CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
+ FlowNDBProperty = _appengine_ndb.FlowNDBProperty
+ _NDB_KEY = _appengine_ndb.NDB_KEY
+ _NDB_MODEL = _appengine_ndb.NDB_MODEL
+ SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
+
+
+def _safe_html(s):
+ """Escape text to make it safe to display.
+
+ Args:
+ s: string, The text to escape.
+
+ Returns:
+ The escaped text as a string.
+ """
+ return cgi.escape(s, quote=1).replace("'", '&#39;')
+
+
+class SiteXsrfSecretKey(db.Model):
+ """Storage for the sites XSRF secret key.
+
+ There will only be one instance stored of this model, the one used for the
+ site.
+ """
+ secret = db.StringProperty()
+
+
+def _generate_new_xsrf_secret_key():
+ """Returns a random XSRF secret key."""
+ return os.urandom(16).encode("hex")
+
+
+def xsrf_secret_key():
+ """Return the secret key for use for XSRF protection.
+
+ If the Site entity does not have a secret key, this method will also create
+ one and persist it.
+
+ Returns:
+ The secret key.
+ """
+ secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
+ if not secret:
+ # Load the one and only instance of SiteXsrfSecretKey.
+ model = SiteXsrfSecretKey.get_or_insert(key_name='site')
+ if not model.secret:
+ model.secret = _generate_new_xsrf_secret_key()
+ model.put()
+ secret = model.secret
+ memcache.add(XSRF_MEMCACHE_ID, secret,
+ namespace=OAUTH2CLIENT_NAMESPACE)
+
+ return str(secret)
+
+
+class AppAssertionCredentials(client.AssertionCredentials):
+ """Credentials object for App Engine Assertion Grants
+
+ This object will allow an App Engine application to identify itself to
+ Google and other OAuth 2.0 servers that can verify assertions. It can be
+ used for the purpose of accessing data stored under an account assigned to
+ the App Engine application itself.
+
+ This credential does not require a flow to instantiate because it
+ represents a two legged flow, and therefore has all of the required
+ information to generate and refresh its own access tokens.
+ """
+
+ @util.positional(2)
+ def __init__(self, scope, **kwargs):
+ """Constructor for AppAssertionCredentials
+
+ Args:
+ scope: string or iterable of strings, scope(s) of the credentials
+ being requested.
+ **kwargs: optional keyword args, including:
+ service_account_id: service account id of the application. If None
+ or unspecified, the default service account for
+ the app is used.
+ """
+ self.scope = util.scopes_to_string(scope)
+ self._kwargs = kwargs
+ self.service_account_id = kwargs.get('service_account_id', None)
+ self._service_account_email = None
+
+ # Assertion type is no longer used, but still in the
+ # parent class signature.
+ super(AppAssertionCredentials, self).__init__(None)
+
+ @classmethod
+ def from_json(cls, json_data):
+ data = json.loads(json_data)
+ return AppAssertionCredentials(data['scope'])
+
+ def _refresh(self, http_request):
+ """Refreshes the access_token.
+
+ Since the underlying App Engine app_identity implementation does its
+ own caching we can skip all the storage hoops and just to a refresh
+ using the API.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ refresh request.
+
+ Raises:
+ AccessTokenRefreshError: When the refresh fails.
+ """
+ try:
+ scopes = self.scope.split()
+ (token, _) = app_identity.get_access_token(
+ scopes, service_account_id=self.service_account_id)
+ except app_identity.Error as e:
+ raise client.AccessTokenRefreshError(str(e))
+ self.access_token = token
+
+ @property
+ def serialization_data(self):
+ raise NotImplementedError('Cannot serialize credentials '
+ 'for Google App Engine.')
+
+ def create_scoped_required(self):
+ return not self.scope
+
+ def create_scoped(self, scopes):
+ return AppAssertionCredentials(scopes, **self._kwargs)
+
+ def sign_blob(self, blob):
+ """Cryptographically sign a blob (of bytes).
+
+ Implements abstract method
+ :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
+
+ Args:
+ blob: bytes, Message to be signed.
+
+ Returns:
+ tuple, A pair of the private key ID used to sign the blob and
+ the signed contents.
+ """
+ return app_identity.sign_blob(blob)
+
+ @property
+ def service_account_email(self):
+ """Get the email for the current service account.
+
+ Returns:
+ string, The email associated with the Google App Engine
+ service account.
+ """
+ if self._service_account_email is None:
+ self._service_account_email = (
+ app_identity.get_service_account_name())
+ return self._service_account_email
+
+
+class FlowProperty(db.Property):
+ """App Engine datastore Property for Flow.
+
+ Utility property that allows easy storage and retrieval of an
+ oauth2client.Flow
+ """
+
+ # Tell what the user type is.
+ data_type = client.Flow
+
+ # For writing to datastore.
+ def get_value_for_datastore(self, model_instance):
+ flow = super(FlowProperty, self).get_value_for_datastore(
+ model_instance)
+ return db.Blob(pickle.dumps(flow))
+
+ # For reading from datastore.
+ def make_value_from_datastore(self, value):
+ if value is None:
+ return None
+ return pickle.loads(value)
+
+ def validate(self, value):
+ if value is not None and not isinstance(value, client.Flow):
+ raise db.BadValueError(
+ 'Property {0} must be convertible '
+ 'to a FlowThreeLegged instance ({1})'.format(self.name, value))
+ return super(FlowProperty, self).validate(value)
+
+ def empty(self, value):
+ return not value
+
+
+class CredentialsProperty(db.Property):
+ """App Engine datastore Property for Credentials.
+
+ Utility property that allows easy storage and retrieval of
+ oauth2client.Credentials
+ """
+
+ # Tell what the user type is.
+ data_type = client.Credentials
+
+ # For writing to datastore.
+ def get_value_for_datastore(self, model_instance):
+ logger.info("get: Got type " + str(type(model_instance)))
+ cred = super(CredentialsProperty, self).get_value_for_datastore(
+ model_instance)
+ if cred is None:
+ cred = ''
+ else:
+ cred = cred.to_json()
+ return db.Blob(cred)
+
+ # For reading from datastore.
+ def make_value_from_datastore(self, value):
+ logger.info("make: Got type " + str(type(value)))
+ if value is None:
+ return None
+ if len(value) == 0:
+ return None
+ try:
+ credentials = client.Credentials.new_from_json(value)
+ except ValueError:
+ credentials = None
+ return credentials
+
+ def validate(self, value):
+ value = super(CredentialsProperty, self).validate(value)
+ logger.info("validate: Got type " + str(type(value)))
+ if value is not None and not isinstance(value, client.Credentials):
+ raise db.BadValueError(
+ 'Property {0} must be convertible '
+ 'to a Credentials instance ({1})'.format(self.name, value))
+ return value
+
+
+class StorageByKeyName(client.Storage):
+ """Store and retrieve a credential to and from the App Engine datastore.
+
+ This Storage helper presumes the Credentials have been stored as a
+ CredentialsProperty or CredentialsNDBProperty on a datastore model class,
+ and that entities are stored by key_name.
+ """
+
+ @util.positional(4)
+ def __init__(self, model, key_name, property_name, cache=None, user=None):
+ """Constructor for Storage.
+
+ Args:
+ model: db.Model or ndb.Model, model class
+ key_name: string, key name for the entity that has the credentials
+ property_name: string, name of the property that is a
+ CredentialsProperty or CredentialsNDBProperty.
+ cache: memcache, a write-through cache to put in front of the
+ datastore. If the model you are using is an NDB model, using
+ a cache will be redundant since the model uses an instance
+ cache and memcache for you.
+ user: users.User object, optional. Can be used to grab user ID as a
+ key_name if no key name is specified.
+ """
+ super(StorageByKeyName, self).__init__()
+
+ if key_name is None:
+ if user is None:
+ raise ValueError('StorageByKeyName called with no '
+ 'key name or user.')
+ key_name = user.user_id()
+
+ self._model = model
+ self._key_name = key_name
+ self._property_name = property_name
+ self._cache = cache
+
+ def _is_ndb(self):
+ """Determine whether the model of the instance is an NDB model.
+
+ Returns:
+ Boolean indicating whether or not the model is an NDB or DB model.
+ """
+ # issubclass will fail if one of the arguments is not a class, only
+ # need worry about new-style classes since ndb and db models are
+ # new-style
+ if isinstance(self._model, type):
+ if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
+ return True
+ elif issubclass(self._model, db.Model):
+ return False
+
+ raise TypeError(
+ 'Model class not an NDB or DB model: {0}.'.format(self._model))
+
+ def _get_entity(self):
+ """Retrieve entity from datastore.
+
+ Uses a different model method for db or ndb models.
+
+ Returns:
+ Instance of the model corresponding to the current storage object
+ and stored using the key name of the storage object.
+ """
+ if self._is_ndb():
+ return self._model.get_by_id(self._key_name)
+ else:
+ return self._model.get_by_key_name(self._key_name)
+
+ def _delete_entity(self):
+ """Delete entity from datastore.
+
+ Attempts to delete using the key_name stored on the object, whether or
+ not the given key is in the datastore.
+ """
+ if self._is_ndb():
+ _NDB_KEY(self._model, self._key_name).delete()
+ else:
+ entity_key = db.Key.from_path(self._model.kind(), self._key_name)
+ db.delete(entity_key)
+
+ @db.non_transactional(allow_existing=True)
+ def locked_get(self):
+ """Retrieve Credential from datastore.
+
+ Returns:
+ oauth2client.Credentials
+ """
+ credentials = None
+ if self._cache:
+ json = self._cache.get(self._key_name)
+ if json:
+ credentials = client.Credentials.new_from_json(json)
+ if credentials is None:
+ entity = self._get_entity()
+ if entity is not None:
+ credentials = getattr(entity, self._property_name)
+ if self._cache:
+ self._cache.set(self._key_name, credentials.to_json())
+
+ if credentials and hasattr(credentials, 'set_store'):
+ credentials.set_store(self)
+ return credentials
+
+ @db.non_transactional(allow_existing=True)
+ def locked_put(self, credentials):
+ """Write a Credentials to the datastore.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ entity = self._model.get_or_insert(self._key_name)
+ setattr(entity, self._property_name, credentials)
+ entity.put()
+ if self._cache:
+ self._cache.set(self._key_name, credentials.to_json())
+
+ @db.non_transactional(allow_existing=True)
+ def locked_delete(self):
+ """Delete Credential from datastore."""
+
+ if self._cache:
+ self._cache.delete(self._key_name)
+
+ self._delete_entity()
+
+
+class CredentialsModel(db.Model):
+ """Storage for OAuth 2.0 Credentials
+
+ Storage of the model is keyed by the user.user_id().
+ """
+ credentials = CredentialsProperty()
+
+
+def _build_state_value(request_handler, user):
+ """Composes the value for the 'state' parameter.
+
+ Packs the current request URI and an XSRF token into an opaque string that
+ can be passed to the authentication server via the 'state' parameter.
+
+ Args:
+ request_handler: webapp.RequestHandler, The request.
+ user: google.appengine.api.users.User, The current user.
+
+ Returns:
+ The state value as a string.
+ """
+ uri = request_handler.request.url
+ token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
+ action_id=str(uri))
+ return uri + ':' + token
+
+
+def _parse_state_value(state, user):
+ """Parse the value of the 'state' parameter.
+
+ Parses the value and validates the XSRF token in the state parameter.
+
+ Args:
+ state: string, The value of the state parameter.
+ user: google.appengine.api.users.User, The current user.
+
+ Returns:
+ The redirect URI, or None if XSRF token is not valid.
+ """
+ uri, token = state.rsplit(':', 1)
+ if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
+ action_id=uri):
+ return uri
+ else:
+ return None
+
+
+class OAuth2Decorator(object):
+ """Utility for making OAuth 2.0 easier.
+
+ Instantiate and then use with oauth_required or oauth_aware
+ as decorators on webapp.RequestHandler methods.
+
+ ::
+
+ decorator = OAuth2Decorator(
+ client_id='837...ent.com',
+ client_secret='Qh...wwI',
+ scope='https://www.googleapis.com/auth/plus')
+
+ class MainHandler(webapp.RequestHandler):
+ @decorator.oauth_required
+ def get(self):
+ http = decorator.http()
+ # http is authorized with the user's Credentials and can be
+ # used in API calls
+
+ """
+
+ def set_credentials(self, credentials):
+ self._tls.credentials = credentials
+
+ def get_credentials(self):
+ """A thread local Credentials object.
+
+ Returns:
+ A client.Credentials object, or None if credentials hasn't been set
+ in this thread yet, which may happen when calling has_credentials
+ inside oauth_aware.
+ """
+ return getattr(self._tls, 'credentials', None)
+
+ credentials = property(get_credentials, set_credentials)
+
+ def set_flow(self, flow):
+ self._tls.flow = flow
+
+ def get_flow(self):
+ """A thread local Flow object.
+
+ Returns:
+ A credentials.Flow object, or None if the flow hasn't been set in
+ this thread yet, which happens in _create_flow() since Flows are
+ created lazily.
+ """
+ return getattr(self._tls, 'flow', None)
+
+ flow = property(get_flow, set_flow)
+
+ @util.positional(4)
+ def __init__(self, client_id, client_secret, scope,
+ auth_uri=oauth2client.GOOGLE_AUTH_URI,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+ user_agent=None,
+ message=None,
+ callback_path='/oauth2callback',
+ token_response_param=None,
+ _storage_class=StorageByKeyName,
+ _credentials_class=CredentialsModel,
+ _credentials_property_name='credentials',
+ **kwargs):
+ """Constructor for OAuth2Decorator
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string client secret.
+ scope: string or iterable of strings, scope(s) of the credentials
+ being requested.
+ auth_uri: string, URI for authorization endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider
+ can be used.
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ revoke_uri: string, URI for revoke endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0
+ provider can be used.
+ user_agent: string, User agent of your application, default to
+ None.
+ message: Message to display if there are problems with the
+ OAuth 2.0 configuration. The message may contain HTML and
+ will be presented on the web interface for any method that
+ uses the decorator.
+ callback_path: string, The absolute path to use as the callback
+ URI. Note that this must match up with the URI given
+ when registering the application in the APIs
+ Console.
+ token_response_param: string. If provided, the full JSON response
+ to the access token request will be encoded
+ and included in this query parameter in the
+ callback URI. This is useful with providers
+ (e.g. wordpress.com) that include extra
+ fields that the client may want.
+ _storage_class: "Protected" keyword argument not typically provided
+ to this constructor. A storage class to aid in
+ storing a Credentials object for a user in the
+ datastore. Defaults to StorageByKeyName.
+ _credentials_class: "Protected" keyword argument not typically
+ provided to this constructor. A db or ndb Model
+ class to hold credentials. Defaults to
+ CredentialsModel.
+ _credentials_property_name: "Protected" keyword argument not
+ typically provided to this constructor.
+ A string indicating the name of the
+ field on the _credentials_class where a
+ Credentials object will be stored.
+ Defaults to 'credentials'.
+ **kwargs: dict, Keyword arguments are passed along as kwargs to
+ the OAuth2WebServerFlow constructor.
+ """
+ self._tls = threading.local()
+ self.flow = None
+ self.credentials = None
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._scope = util.scopes_to_string(scope)
+ self._auth_uri = auth_uri
+ self._token_uri = token_uri
+ self._revoke_uri = revoke_uri
+ self._user_agent = user_agent
+ self._kwargs = kwargs
+ self._message = message
+ self._in_error = False
+ self._callback_path = callback_path
+ self._token_response_param = token_response_param
+ self._storage_class = _storage_class
+ self._credentials_class = _credentials_class
+ self._credentials_property_name = _credentials_property_name
+
+ def _display_error_message(self, request_handler):
+ request_handler.response.out.write('<html><body>')
+ request_handler.response.out.write(_safe_html(self._message))
+ request_handler.response.out.write('</body></html>')
+
+ def oauth_required(self, method):
+ """Decorator that starts the OAuth 2.0 dance.
+
+ Starts the OAuth dance for the logged in user if they haven't already
+ granted access for this application.
+
+ Args:
+ method: callable, to be decorated method of a webapp.RequestHandler
+ instance.
+ """
+
+ def check_oauth(request_handler, *args, **kwargs):
+ if self._in_error:
+ self._display_error_message(request_handler)
+ return
+
+ user = users.get_current_user()
+ # Don't use @login_decorator as this could be used in a
+ # POST request.
+ if not user:
+ request_handler.redirect(users.create_login_url(
+ request_handler.request.uri))
+ return
+
+ self._create_flow(request_handler)
+
+ # Store the request URI in 'state' so we can use it later
+ self.flow.params['state'] = _build_state_value(
+ request_handler, user)
+ self.credentials = self._storage_class(
+ self._credentials_class, None,
+ self._credentials_property_name, user=user).get()
+
+ if not self.has_credentials():
+ return request_handler.redirect(self.authorize_url())
+ try:
+ resp = method(request_handler, *args, **kwargs)
+ except client.AccessTokenRefreshError:
+ return request_handler.redirect(self.authorize_url())
+ finally:
+ self.credentials = None
+ return resp
+
+ return check_oauth
+
+ def _create_flow(self, request_handler):
+ """Create the Flow object.
+
+ The Flow is calculated lazily since we don't know where this app is
+ running until it receives a request, at which point redirect_uri can be
+ calculated and then the Flow object can be constructed.
+
+ Args:
+ request_handler: webapp.RequestHandler, the request handler.
+ """
+ if self.flow is None:
+ redirect_uri = request_handler.request.relative_url(
+ self._callback_path) # Usually /oauth2callback
+ self.flow = client.OAuth2WebServerFlow(
+ self._client_id, self._client_secret, self._scope,
+ redirect_uri=redirect_uri, user_agent=self._user_agent,
+ auth_uri=self._auth_uri, token_uri=self._token_uri,
+ revoke_uri=self._revoke_uri, **self._kwargs)
+
+ def oauth_aware(self, method):
+ """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
+
+ Does all the setup for the OAuth dance, but doesn't initiate it.
+ This decorator is useful if you want to create a page that knows
+ whether or not the user has granted access to this application.
+ From within a method decorated with @oauth_aware the has_credentials()
+ and authorize_url() methods can be called.
+
+ Args:
+ method: callable, to be decorated method of a webapp.RequestHandler
+ instance.
+ """
+
+ def setup_oauth(request_handler, *args, **kwargs):
+ if self._in_error:
+ self._display_error_message(request_handler)
+ return
+
+ user = users.get_current_user()
+ # Don't use @login_decorator as this could be used in a
+ # POST request.
+ if not user:
+ request_handler.redirect(users.create_login_url(
+ request_handler.request.uri))
+ return
+
+ self._create_flow(request_handler)
+
+ self.flow.params['state'] = _build_state_value(request_handler,
+ user)
+ self.credentials = self._storage_class(
+ self._credentials_class, None,
+ self._credentials_property_name, user=user).get()
+ try:
+ resp = method(request_handler, *args, **kwargs)
+ finally:
+ self.credentials = None
+ return resp
+ return setup_oauth
+
+ def has_credentials(self):
+ """True if for the logged in user there are valid access Credentials.
+
+ Must only be called from with a webapp.RequestHandler subclassed method
+ that had been decorated with either @oauth_required or @oauth_aware.
+ """
+ return self.credentials is not None and not self.credentials.invalid
+
+ def authorize_url(self):
+ """Returns the URL to start the OAuth dance.
+
+ Must only be called from with a webapp.RequestHandler subclassed method
+ that had been decorated with either @oauth_required or @oauth_aware.
+ """
+ url = self.flow.step1_get_authorize_url()
+ return str(url)
+
+ def http(self, *args, **kwargs):
+ """Returns an authorized http instance.
+
+ Must only be called from within an @oauth_required decorated method, or
+ from within an @oauth_aware decorated method where has_credentials()
+ returns True.
+
+ Args:
+ *args: Positional arguments passed to httplib2.Http constructor.
+ **kwargs: Positional arguments passed to httplib2.Http constructor.
+ """
+ return self.credentials.authorize(httplib2.Http(*args, **kwargs))
+
+ @property
+ def callback_path(self):
+ """The absolute path where the callback will occur.
+
+ Note this is the absolute path, not the absolute URI, that will be
+ calculated by the decorator at runtime. See callback_handler() for how
+ this should be used.
+
+ Returns:
+ The callback path as a string.
+ """
+ return self._callback_path
+
+ def callback_handler(self):
+ """RequestHandler for the OAuth 2.0 redirect callback.
+
+ Usage::
+
+ app = webapp.WSGIApplication([
+ ('/index', MyIndexHandler),
+ ...,
+ (decorator.callback_path, decorator.callback_handler())
+ ])
+
+ Returns:
+ A webapp.RequestHandler that handles the redirect back from the
+ server during the OAuth 2.0 dance.
+ """
+ decorator = self
+
+ class OAuth2Handler(webapp.RequestHandler):
+ """Handler for the redirect_uri of the OAuth 2.0 dance."""
+
+ @login_required
+ def get(self):
+ error = self.request.get('error')
+ if error:
+ errormsg = self.request.get('error_description', error)
+ self.response.out.write(
+ 'The authorization request failed: {0}'.format(
+ _safe_html(errormsg)))
+ else:
+ user = users.get_current_user()
+ decorator._create_flow(self)
+ credentials = decorator.flow.step2_exchange(
+ self.request.params)
+ decorator._storage_class(
+ decorator._credentials_class, None,
+ decorator._credentials_property_name,
+ user=user).put(credentials)
+ redirect_uri = _parse_state_value(
+ str(self.request.get('state')), user)
+ if redirect_uri is None:
+ self.response.out.write(
+ 'The authorization request failed')
+ return
+
+ if (decorator._token_response_param and
+ credentials.token_response):
+ resp_json = json.dumps(credentials.token_response)
+ redirect_uri = util._add_query_parameter(
+ redirect_uri, decorator._token_response_param,
+ resp_json)
+
+ self.redirect(redirect_uri)
+
+ return OAuth2Handler
+
+ def callback_application(self):
+ """WSGI application for handling the OAuth 2.0 redirect callback.
+
+ If you need finer grained control use `callback_handler` which returns
+ just the webapp.RequestHandler.
+
+ Returns:
+ A webapp.WSGIApplication that handles the redirect back from the
+ server during the OAuth 2.0 dance.
+ """
+ return webapp.WSGIApplication([
+ (self.callback_path, self.callback_handler())
+ ])
+
+
+class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
+ """An OAuth2Decorator that builds from a clientsecrets file.
+
+ Uses a clientsecrets file as the source for all the information when
+ constructing an OAuth2Decorator.
+
+ ::
+
+ decorator = OAuth2DecoratorFromClientSecrets(
+ os.path.join(os.path.dirname(__file__), 'client_secrets.json')
+ scope='https://www.googleapis.com/auth/plus')
+
+ class MainHandler(webapp.RequestHandler):
+ @decorator.oauth_required
+ def get(self):
+ http = decorator.http()
+ # http is authorized with the user's Credentials and can be
+ # used in API calls
+
+ """
+
+ @util.positional(3)
+ def __init__(self, filename, scope, message=None, cache=None, **kwargs):
+ """Constructor
+
+ Args:
+ filename: string, File name of client secrets.
+ scope: string or iterable of strings, scope(s) of the credentials
+ being requested.
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. The message may
+ contain HTML and will be presented on the web interface
+ for any method that uses the decorator.
+ cache: An optional cache service client that implements get() and
+ set()
+ methods. See clientsecrets.loadfile() for details.
+ **kwargs: dict, Keyword arguments are passed along as kwargs to
+ the OAuth2WebServerFlow constructor.
+ """
+ client_type, client_info = clientsecrets.loadfile(filename,
+ cache=cache)
+ if client_type not in (clientsecrets.TYPE_WEB,
+ clientsecrets.TYPE_INSTALLED):
+ raise clientsecrets.InvalidClientSecretsError(
+ "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
+
+ constructor_kwargs = dict(kwargs)
+ constructor_kwargs.update({
+ 'auth_uri': client_info['auth_uri'],
+ 'token_uri': client_info['token_uri'],
+ 'message': message,
+ })
+ revoke_uri = client_info.get('revoke_uri')
+ if revoke_uri is not None:
+ constructor_kwargs['revoke_uri'] = revoke_uri
+ super(OAuth2DecoratorFromClientSecrets, self).__init__(
+ client_info['client_id'], client_info['client_secret'],
+ scope, **constructor_kwargs)
+ if message is not None:
+ self._message = message
+ else:
+ self._message = 'Please configure your application for OAuth 2.0.'
+
+
+@util.positional(2)
+def oauth2decorator_from_clientsecrets(filename, scope,
+ message=None, cache=None):
+ """Creates an OAuth2Decorator populated from a clientsecrets file.
+
+ Args:
+ filename: string, File name of client secrets.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. The message may
+ contain HTML and will be presented on the web interface for
+ any method that uses the decorator.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+
+ Returns: An OAuth2Decorator
+ """
+ return OAuth2DecoratorFromClientSecrets(filename, scope,
+ message=message, cache=cache)
diff --git a/oauth2client/contrib/devshell.py b/oauth2client/contrib/devshell.py
new file mode 100644
index 0000000..b8bb978
--- /dev/null
+++ b/oauth2client/contrib/devshell.py
@@ -0,0 +1,146 @@
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""OAuth 2.0 utitilies for Google Developer Shell environment."""
+
+import datetime
+import json
+import os
+import socket
+
+from oauth2client import _helpers
+from oauth2client import client
+
+DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
+
+
+class Error(Exception):
+ """Errors for this module."""
+ pass
+
+
+class CommunicationError(Error):
+ """Errors for communication with the Developer Shell server."""
+
+
+class NoDevshellServer(Error):
+ """Error when no Developer Shell server can be contacted."""
+
+# The request for credential information to the Developer Shell client socket
+# is always an empty PBLite-formatted JSON object, so just define it as a
+# constant.
+CREDENTIAL_INFO_REQUEST_JSON = '[]'
+
+
+class CredentialInfoResponse(object):
+ """Credential information response from Developer Shell server.
+
+ The credential information response from Developer Shell socket is a
+ PBLite-formatted JSON array with fields encoded by their index in the
+ array:
+
+ * Index 0 - user email
+ * Index 1 - default project ID. None if the project context is not known.
+ * Index 2 - OAuth2 access token. None if there is no valid auth context.
+ * Index 3 - Seconds until the access token expires. None if not present.
+ """
+
+ def __init__(self, json_string):
+ """Initialize the response data from JSON PBLite array."""
+ pbl = json.loads(json_string)
+ if not isinstance(pbl, list):
+ raise ValueError('Not a list: ' + str(pbl))
+ pbl_len = len(pbl)
+ self.user_email = pbl[0] if pbl_len > 0 else None
+ self.project_id = pbl[1] if pbl_len > 1 else None
+ self.access_token = pbl[2] if pbl_len > 2 else None
+ self.expires_in = pbl[3] if pbl_len > 3 else None
+
+
+def _SendRecv():
+ """Communicate with the Developer Shell server socket."""
+
+ port = int(os.getenv(DEVSHELL_ENV, 0))
+ if port == 0:
+ raise NoDevshellServer()
+
+ sock = socket.socket()
+ sock.connect(('localhost', port))
+
+ data = CREDENTIAL_INFO_REQUEST_JSON
+ msg = '{0}\n{1}'.format(len(data), data)
+ sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
+
+ header = sock.recv(6).decode()
+ if '\n' not in header:
+ raise CommunicationError('saw no newline in the first 6 bytes')
+ len_str, json_str = header.split('\n', 1)
+ to_read = int(len_str) - len(json_str)
+ if to_read > 0:
+ json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
+
+ return CredentialInfoResponse(json_str)
+
+
+class DevshellCredentials(client.GoogleCredentials):
+ """Credentials object for Google Developer Shell environment.
+
+ This object will allow a Google Developer Shell session to identify its
+ user to Google and other OAuth 2.0 servers that can verify assertions. It
+ can be used for the purpose of accessing data stored under the user
+ account.
+
+ This credential does not require a flow to instantiate because it
+ represents a two legged flow, and therefore has all of the required
+ information to generate and refresh its own access tokens.
+ """
+
+ def __init__(self, user_agent=None):
+ super(DevshellCredentials, self).__init__(
+ None, # access_token, initialized below
+ None, # client_id
+ None, # client_secret
+ None, # refresh_token
+ None, # token_expiry
+ None, # token_uri
+ user_agent)
+ self._refresh(None)
+
+ def _refresh(self, http_request):
+ self.devshell_response = _SendRecv()
+ self.access_token = self.devshell_response.access_token
+ expires_in = self.devshell_response.expires_in
+ if expires_in is not None:
+ delta = datetime.timedelta(seconds=expires_in)
+ self.token_expiry = client._UTCNOW() + delta
+ else:
+ self.token_expiry = None
+
+ @property
+ def user_email(self):
+ return self.devshell_response.user_email
+
+ @property
+ def project_id(self):
+ return self.devshell_response.project_id
+
+ @classmethod
+ def from_json(cls, json_data):
+ raise NotImplementedError(
+ 'Cannot load Developer Shell credentials from JSON.')
+
+ @property
+ def serialization_data(self):
+ raise NotImplementedError(
+ 'Cannot serialize Developer Shell credentials.')
diff --git a/oauth2client/contrib/dictionary_storage.py b/oauth2client/contrib/dictionary_storage.py
new file mode 100644
index 0000000..6ee333f
--- /dev/null
+++ b/oauth2client/contrib/dictionary_storage.py
@@ -0,0 +1,65 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Dictionary storage for OAuth2 Credentials."""
+
+from oauth2client import client
+
+
+class DictionaryStorage(client.Storage):
+ """Store and retrieve credentials to and from a dictionary-like object.
+
+ Args:
+ dictionary: A dictionary or dictionary-like object.
+ key: A string or other hashable. The credentials will be stored in
+ ``dictionary[key]``.
+ lock: An optional threading.Lock-like object. The lock will be
+ acquired before anything is written or read from the
+ dictionary.
+ """
+
+ def __init__(self, dictionary, key, lock=None):
+ """Construct a DictionaryStorage instance."""
+ super(DictionaryStorage, self).__init__(lock=lock)
+ self._dictionary = dictionary
+ self._key = key
+
+ def locked_get(self):
+ """Retrieve the credentials from the dictionary, if they exist.
+
+ Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
+ """
+ serialized = self._dictionary.get(self._key)
+
+ if serialized is None:
+ return None
+
+ credentials = client.OAuth2Credentials.from_json(serialized)
+ credentials.set_store(self)
+
+ return credentials
+
+ def locked_put(self, credentials):
+ """Save the credentials to the dictionary.
+
+ Args:
+ credentials: A :class:`oauth2client.client.OAuth2Credentials`
+ instance.
+ """
+ serialized = credentials.to_json()
+ self._dictionary[self._key] = serialized
+
+ def locked_delete(self):
+ """Remove the credentials from the dictionary, if they exist."""
+ self._dictionary.pop(self._key, None)
diff --git a/oauth2client/contrib/django_util/__init__.py b/oauth2client/contrib/django_util/__init__.py
new file mode 100644
index 0000000..5449e32
--- /dev/null
+++ b/oauth2client/contrib/django_util/__init__.py
@@ -0,0 +1,477 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for the Django web framework.
+
+Provides Django views and helpers the make using the OAuth2 web server
+flow easier. It includes an ``oauth_required`` decorator to automatically
+ensure that user credentials are available, and an ``oauth_enabled`` decorator
+to check if the user has authorized, and helper shortcuts to create the
+authorization URL otherwise.
+
+There are two basic use cases supported. The first is using Google OAuth as the
+primary form of authentication, which is the simpler approach recommended
+for applications without their own user system.
+
+The second use case is adding Google OAuth credentials to an
+existing Django model containing a Django user field. Most of the
+configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in
+settings.py. See "Adding Credentials To An Existing Django User System" for
+usage differences.
+
+Only Django versions 1.8+ are supported.
+
+Configuration
+===============
+
+To configure, you'll need a set of OAuth2 web application credentials from
+`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
+
+Add the helper to your INSTALLED_APPS:
+
+.. code-block:: python
+ :caption: settings.py
+ :name: installed_apps
+
+ INSTALLED_APPS = (
+ # other apps
+ "django.contrib.sessions.middleware"
+ "oauth2client.contrib.django_util"
+ )
+
+This helper also requires the Django Session Middleware, so
+``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
+
+Add the client secrets created earlier to the settings. You can either
+specify the path to the credentials file in JSON format
+
+.. code-block:: python
+ :caption: settings.py
+ :name: secrets_file
+
+ GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
+
+Or, directly configure the client Id and client secret.
+
+
+.. code-block:: python
+ :caption: settings.py
+ :name: secrets_config
+
+ GOOGLE_OAUTH2_CLIENT_ID=client-id-field
+ GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
+
+By default, the default scopes for the required decorator only contains the
+``email`` scopes. You can change that default in the settings.
+
+.. code-block:: python
+ :caption: settings.py
+ :name: scopes
+
+ GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
+
+By default, the decorators will add an `oauth` object to the Django request
+object, and include all of its state and helpers inside that object. If the
+`oauth` name conflicts with another usage, it can be changed
+
+.. code-block:: python
+ :caption: settings.py
+ :name: request_prefix
+
+ # changes request.oauth to request.google_oauth
+ GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
+
+Add the oauth2 routes to your application's urls.py urlpatterns.
+
+.. code-block:: python
+ :caption: urls.py
+ :name: urls
+
+ from oauth2client.contrib.django_util.site import urls as oauth2_urls
+
+ urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
+
+To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
+This creates a credentials object with an id_token, and allows you to create
+an `http` object to build service clients with. These are all attached to the
+request.oauth
+
+.. code-block:: python
+ :caption: views.py
+ :name: views_required
+
+ from oauth2client.contrib.django_util.decorators import oauth_required
+
+ @oauth_required
+ def requires_default_scopes(request):
+ email = request.oauth.credentials.id_token['email']
+ service = build(serviceName='calendar', version='v3',
+ http=request.oauth.http,
+ developerKey=API_KEY)
+ events = service.events().list(calendarId='primary').execute()['items']
+ return HttpResponse("email: {0} , calendar: {1}".format(
+ email,str(events)))
+ return HttpResponse(
+ "email: {0} , calendar: {1}".format(email, str(events)))
+
+To make OAuth2 optional and provide an authorization link in your own views.
+
+.. code-block:: python
+ :caption: views.py
+ :name: views_enabled2
+
+ from oauth2client.contrib.django_util.decorators import oauth_enabled
+
+ @oauth_enabled
+ def optional_oauth2(request):
+ if request.oauth.has_credentials():
+ # this could be passed into a view
+ # request.oauth.http is also initialized
+ return HttpResponse("User email: {0}".format(
+ request.oauth.credentials.id_token['email']))
+ else:
+ return HttpResponse(
+ 'Here is an OAuth Authorize link: <a href="{0}">Authorize'
+ '</a>'.format(request.oauth.get_authorize_redirect()))
+
+If a view needs a scope not included in the default scopes specified in
+the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
+and specify additional scopes in the decorator arguments.
+
+.. code-block:: python
+ :caption: views.py
+ :name: views_required_additional_scopes
+
+ @oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
+ def drive_required(request):
+ if request.oauth.has_credentials():
+ service = build(serviceName='drive', version='v2',
+ http=request.oauth.http,
+ developerKey=API_KEY)
+ events = service.files().list().execute()['items']
+ return HttpResponse(str(events))
+ else:
+ return HttpResponse(
+ 'Here is an OAuth Authorize link: <a href="{0}">Authorize'
+ '</a>'.format(request.oauth.get_authorize_redirect()))
+
+
+To provide a callback on authorization being completed, use the
+oauth2_authorized signal:
+
+.. code-block:: python
+ :caption: views.py
+ :name: signals
+
+ from oauth2client.contrib.django_util.signals import oauth2_authorized
+
+ def test_callback(sender, request, credentials, **kwargs):
+ print("Authorization Signal Received {0}".format(
+ credentials.id_token['email']))
+
+ oauth2_authorized.connect(test_callback)
+
+Adding Credentials To An Existing Django User System
+=====================================================
+
+As an alternative to storing the credentials in the session, the helper
+can be configured to store the fields on a Django model. This might be useful
+if you need to use the credentials outside the context of a user request. It
+also prevents the need for a logged in user to repeat the OAuth flow when
+starting a new session.
+
+To use, change ``settings.py``
+
+.. code-block:: python
+ :caption: settings.py
+ :name: storage_model_config
+
+ GOOGLE_OAUTH2_STORAGE_MODEL = {
+ 'model': 'path.to.model.MyModel',
+ 'user_property': 'user_id',
+ 'credentials_property': 'credential'
+ }
+
+Where ``path.to.model`` class is the fully qualified name of a
+``django.db.model`` class containing a ``django.contrib.auth.models.User``
+field with the name specified by `user_property` and a
+:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name
+specified by `credentials_property`. For the sample configuration given,
+our model would look like
+
+.. code-block:: python
+ :caption: models.py
+ :name: storage_model_model
+
+ from django.contrib.auth.models import User
+ from oauth2client.contrib.django_util.models import CredentialsField
+
+ class MyModel(models.Model):
+ # ... other fields here ...
+ user = models.OneToOneField(User)
+ credential = CredentialsField()
+"""
+
+import importlib
+
+import django.conf
+from django.core import exceptions
+from django.core import urlresolvers
+import httplib2
+from six.moves.urllib import parse
+
+from oauth2client import clientsecrets
+from oauth2client.contrib import dictionary_storage
+from oauth2client.contrib.django_util import storage
+
+GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
+GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
+
+
+def _load_client_secrets(filename):
+ """Loads client secrets from the given filename.
+
+ Args:
+ filename: The name of the file containing the JSON secret key.
+
+ Returns:
+ A 2-tuple, the first item containing the client id, and the second
+ item containing a client secret.
+ """
+ client_type, client_info = clientsecrets.loadfile(filename)
+
+ if client_type != clientsecrets.TYPE_WEB:
+ raise ValueError(
+ 'The flow specified in {} is not supported, only the WEB flow '
+ 'type is supported.'.format(client_type))
+ return client_info['client_id'], client_info['client_secret']
+
+
+def _get_oauth2_client_id_and_secret(settings_instance):
+ """Initializes client id and client secret based on the settings.
+
+ Args:
+ settings_instance: An instance of ``django.conf.settings``.
+
+ Returns:
+ A 2-tuple, the first item is the client id and the second
+ item is the client secret.
+ """
+ secret_json = getattr(settings_instance,
+ 'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
+ if secret_json is not None:
+ return _load_client_secrets(secret_json)
+ else:
+ client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
+ None)
+ client_secret = getattr(settings_instance,
+ "GOOGLE_OAUTH2_CLIENT_SECRET", None)
+ if client_id is not None and client_secret is not None:
+ return client_id, client_secret
+ else:
+ raise exceptions.ImproperlyConfigured(
+ "Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
+ "both GOOGLE_OAUTH2_CLIENT_ID and "
+ "GOOGLE_OAUTH2_CLIENT_SECRET in settings.py")
+
+
+def _get_storage_model():
+ """This configures whether the credentials will be stored in the session
+ or the Django ORM based on the settings. By default, the credentials
+ will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL`
+ is found in the settings. Usually, the ORM storage is used to integrate
+ credentials into an existing Django user system.
+
+ Returns:
+ A tuple containing three strings, or None. If
+ ``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple
+ will contain the fully qualifed path of the `django.db.model`,
+ the name of the ``django.contrib.auth.models.User`` field on the
+ model, and the name of the
+ :class:`oauth2client.contrib.django_util.models.CredentialsField`
+ field on the model. If Django ORM storage is not configured,
+ this function returns None.
+ """
+ storage_model_settings = getattr(django.conf.settings,
+ 'GOOGLE_OAUTH2_STORAGE_MODEL', None)
+ if storage_model_settings is not None:
+ return (storage_model_settings['model'],
+ storage_model_settings['user_property'],
+ storage_model_settings['credentials_property'])
+ else:
+ return None, None, None
+
+
+class OAuth2Settings(object):
+ """Initializes Django OAuth2 Helper Settings
+
+ This class loads the OAuth2 Settings from the Django settings, and then
+ provides those settings as attributes to the rest of the views and
+ decorators in the module.
+
+ Attributes:
+ scopes: A list of OAuth2 scopes that the decorators and views will use
+ as defaults.
+ request_prefix: The name of the attribute that the decorators use to
+ attach the UserOAuth2 object to the Django request object.
+ client_id: The OAuth2 Client ID.
+ client_secret: The OAuth2 Client Secret.
+ """
+
+ def __init__(self, settings_instance):
+ self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
+ GOOGLE_OAUTH2_DEFAULT_SCOPES)
+ self.request_prefix = getattr(settings_instance,
+ 'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
+ GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
+ self.client_id, self.client_secret = \
+ _get_oauth2_client_id_and_secret(settings_instance)
+
+ if ('django.contrib.sessions.middleware.SessionMiddleware'
+ not in settings_instance.MIDDLEWARE_CLASSES):
+ raise exceptions.ImproperlyConfigured(
+ 'The Google OAuth2 Helper requires session middleware to '
+ 'be installed. Edit your MIDDLEWARE_CLASSES setting'
+ ' to include \'django.contrib.sessions.middleware.'
+ 'SessionMiddleware\'.')
+ (self.storage_model, self.storage_model_user_property,
+ self.storage_model_credentials_property) = _get_storage_model()
+
+
+oauth2_settings = OAuth2Settings(django.conf.settings)
+
+_CREDENTIALS_KEY = 'google_oauth2_credentials'
+
+
+def get_storage(request):
+ """ Gets a Credentials storage object provided by the Django OAuth2 Helper
+ object.
+
+ Args:
+ request: Reference to the current request object.
+
+ Returns:
+ An :class:`oauth2.client.Storage` object.
+ """
+ storage_model = oauth2_settings.storage_model
+ user_property = oauth2_settings.storage_model_user_property
+ credentials_property = oauth2_settings.storage_model_credentials_property
+
+ if storage_model:
+ module_name, class_name = storage_model.rsplit('.', 1)
+ module = importlib.import_module(module_name)
+ storage_model_class = getattr(module, class_name)
+ return storage.DjangoORMStorage(storage_model_class,
+ user_property,
+ request.user,
+ credentials_property)
+ else:
+ # use session
+ return dictionary_storage.DictionaryStorage(
+ request.session, key=_CREDENTIALS_KEY)
+
+
+def _redirect_with_params(url_name, *args, **kwargs):
+ """Helper method to create a redirect response with URL params.
+
+ This builds a redirect string that converts kwargs into a
+ query string.
+
+ Args:
+ url_name: The name of the url to redirect to.
+ kwargs: the query string param and their values to build.
+
+ Returns:
+ A properly formatted redirect string.
+ """
+ url = urlresolvers.reverse(url_name, args=args)
+ params = parse.urlencode(kwargs, True)
+ return "{0}?{1}".format(url, params)
+
+
+def _credentials_from_request(request):
+ """Gets the authorized credentials for this flow, if they exist."""
+ # ORM storage requires a logged in user
+ if (oauth2_settings.storage_model is None or
+ request.user.is_authenticated()):
+ return get_storage(request).get()
+ else:
+ return None
+
+
+class UserOAuth2(object):
+ """Class to create oauth2 objects on Django request objects containing
+ credentials and helper methods.
+ """
+
+ def __init__(self, request, scopes=None, return_url=None):
+ """Initialize the Oauth2 Object.
+
+ Args:
+ request: Django request object.
+ scopes: Scopes desired for this OAuth2 flow.
+ return_url: The url to return to after the OAuth flow is complete,
+ defaults to the request's current URL path.
+ """
+ self.request = request
+ self.return_url = return_url or request.get_full_path()
+ if scopes:
+ self._scopes = set(oauth2_settings.scopes) | set(scopes)
+ else:
+ self._scopes = set(oauth2_settings.scopes)
+
+ def get_authorize_redirect(self):
+ """Creates a URl to start the OAuth2 authorization flow."""
+ get_params = {
+ 'return_url': self.return_url,
+ 'scopes': self._get_scopes()
+ }
+
+ return _redirect_with_params('google_oauth:authorize', **get_params)
+
+ def has_credentials(self):
+ """Returns True if there are valid credentials for the current user
+ and required scopes."""
+ credentials = _credentials_from_request(self.request)
+ return (credentials and not credentials.invalid and
+ credentials.has_scopes(self._get_scopes()))
+
+ def _get_scopes(self):
+ """Returns the scopes associated with this object, kept up to
+ date for incremental auth."""
+ if _credentials_from_request(self.request):
+ return (self._scopes |
+ _credentials_from_request(self.request).scopes)
+ else:
+ return self._scopes
+
+ @property
+ def scopes(self):
+ """Returns the scopes associated with this OAuth2 object."""
+ # make sure previously requested custom scopes are maintained
+ # in future authorizations
+ return self._get_scopes()
+
+ @property
+ def credentials(self):
+ """Gets the authorized credentials for this flow, if they exist."""
+ return _credentials_from_request(self.request)
+
+ @property
+ def http(self):
+ """Helper method to create an HTTP client authorized with OAuth2
+ credentials."""
+ if self.has_credentials():
+ return self.credentials.authorize(httplib2.Http())
+ return None
diff --git a/oauth2client/contrib/django_util/apps.py b/oauth2client/contrib/django_util/apps.py
new file mode 100644
index 0000000..86676b9
--- /dev/null
+++ b/oauth2client/contrib/django_util/apps.py
@@ -0,0 +1,32 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Application Config For Django OAuth2 Helper.
+
+Django 1.7+ provides an
+[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
+API so that Django projects can introspect on installed applications using a
+stable API. This module exists to follow that convention.
+"""
+
+import sys
+
+# Django 1.7+ only supports Python 2.7+
+if sys.hexversion >= 0x02070000: # pragma: NO COVER
+ from django.apps import AppConfig
+
+ class GoogleOAuth2HelperConfig(AppConfig):
+ """ App Config for Django Helper"""
+ name = 'oauth2client.django_util'
+ verbose_name = "Google OAuth2 Django Helper"
diff --git a/oauth2client/contrib/django_util/decorators.py b/oauth2client/contrib/django_util/decorators.py
new file mode 100644
index 0000000..e62e171
--- /dev/null
+++ b/oauth2client/contrib/django_util/decorators.py
@@ -0,0 +1,145 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Decorators for Django OAuth2 Flow.
+
+Contains two decorators, ``oauth_required`` and ``oauth_enabled``.
+
+``oauth_required`` will ensure that a user has an oauth object containing
+credentials associated with the request, and if not, redirect to the
+authorization flow.
+
+``oauth_enabled`` will attach the oauth2 object containing credentials if it
+exists. If it doesn't, the view will still render, but helper methods will be
+attached to start the oauth2 flow.
+"""
+
+from django import shortcuts
+import django.conf
+from six import wraps
+from six.moves.urllib import parse
+
+from oauth2client.contrib import django_util
+
+
+def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
+ """ Decorator to require OAuth2 credentials for a view.
+
+
+ .. code-block:: python
+ :caption: views.py
+ :name: views_required_2
+
+
+ from oauth2client.django_util.decorators import oauth_required
+
+ @oauth_required
+ def requires_default_scopes(request):
+ email = request.credentials.id_token['email']
+ service = build(serviceName='calendar', version='v3',
+ http=request.oauth.http,
+ developerKey=API_KEY)
+ events = service.events().list(
+ calendarId='primary').execute()['items']
+ return HttpResponse(
+ "email: {0}, calendar: {1}".format(email, str(events)))
+
+ Args:
+ decorated_function: View function to decorate, must have the Django
+ request object as the first argument.
+ scopes: Scopes to require, will default.
+ decorator_kwargs: Can include ``return_url`` to specify the URL to
+ return to after OAuth2 authorization is complete.
+
+ Returns:
+ An OAuth2 Authorize view if credentials are not found or if the
+ credentials are missing the required scopes. Otherwise,
+ the decorated view.
+ """
+ def curry_wrapper(wrapped_function):
+ @wraps(wrapped_function)
+ def required_wrapper(request, *args, **kwargs):
+ if not (django_util.oauth2_settings.storage_model is None or
+ request.user.is_authenticated()):
+ redirect_str = '{0}?next={1}'.format(
+ django.conf.settings.LOGIN_URL,
+ parse.quote(request.path))
+ return shortcuts.redirect(redirect_str)
+
+ return_url = decorator_kwargs.pop('return_url',
+ request.get_full_path())
+ user_oauth = django_util.UserOAuth2(request, scopes, return_url)
+ if not user_oauth.has_credentials():
+ return shortcuts.redirect(user_oauth.get_authorize_redirect())
+ setattr(request, django_util.oauth2_settings.request_prefix,
+ user_oauth)
+ return wrapped_function(request, *args, **kwargs)
+
+ return required_wrapper
+
+ if decorated_function:
+ return curry_wrapper(decorated_function)
+ else:
+ return curry_wrapper
+
+
+def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
+ """ Decorator to enable OAuth Credentials if authorized, and setup
+ the oauth object on the request object to provide helper functions
+ to start the flow otherwise.
+
+ .. code-block:: python
+ :caption: views.py
+ :name: views_enabled3
+
+ from oauth2client.django_util.decorators import oauth_enabled
+
+ @oauth_enabled
+ def optional_oauth2(request):
+ if request.oauth.has_credentials():
+ # this could be passed into a view
+ # request.oauth.http is also initialized
+ return HttpResponse("User email: {0}".format(
+ request.oauth.credentials.id_token['email'])
+ else:
+ return HttpResponse('Here is an OAuth Authorize link:
+ <a href="{0}">Authorize</a>'.format(
+ request.oauth.get_authorize_redirect()))
+
+
+ Args:
+ decorated_function: View function to decorate.
+ scopes: Scopes to require, will default.
+ decorator_kwargs: Can include ``return_url`` to specify the URL to
+ return to after OAuth2 authorization is complete.
+
+ Returns:
+ The decorated view function.
+ """
+ def curry_wrapper(wrapped_function):
+ @wraps(wrapped_function)
+ def enabled_wrapper(request, *args, **kwargs):
+ return_url = decorator_kwargs.pop('return_url',
+ request.get_full_path())
+ user_oauth = django_util.UserOAuth2(request, scopes, return_url)
+ setattr(request, django_util.oauth2_settings.request_prefix,
+ user_oauth)
+ return wrapped_function(request, *args, **kwargs)
+
+ return enabled_wrapper
+
+ if decorated_function:
+ return curry_wrapper(decorated_function)
+ else:
+ return curry_wrapper
diff --git a/oauth2client/contrib/django_util/models.py b/oauth2client/contrib/django_util/models.py
new file mode 100644
index 0000000..87e1da7
--- /dev/null
+++ b/oauth2client/contrib/django_util/models.py
@@ -0,0 +1,75 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Contains classes used for the Django ORM storage."""
+
+import base64
+import pickle
+
+from django.db import models
+from django.utils import encoding
+
+import oauth2client
+
+
+class CredentialsField(models.Field):
+ """Django ORM field for storing OAuth2 Credentials."""
+
+ def __init__(self, *args, **kwargs):
+ if 'null' not in kwargs:
+ kwargs['null'] = True
+ super(CredentialsField, self).__init__(*args, **kwargs)
+
+ def get_internal_type(self):
+ return 'BinaryField'
+
+ def from_db_value(self, value, expression, connection, context):
+ """Overrides ``models.Field`` method. This converts the value
+ returned from the database to an instance of this class.
+ """
+ return self.to_python(value)
+
+ def to_python(self, value):
+ """Overrides ``models.Field`` method. This is used to convert
+ bytes (from serialization etc) to an instance of this class"""
+ if value is None:
+ return None
+ elif isinstance(value, oauth2client.client.Credentials):
+ return value
+ else:
+ return pickle.loads(base64.b64decode(encoding.smart_bytes(value)))
+
+ def get_prep_value(self, value):
+ """Overrides ``models.Field`` method. This is used to convert
+ the value from an instances of this class to bytes that can be
+ inserted into the database.
+ """
+ if value is None:
+ return None
+ else:
+ return encoding.smart_text(base64.b64encode(pickle.dumps(value)))
+
+ def value_to_string(self, obj):
+ """Convert the field value from the provided model to a string.
+
+ Used during model serialization.
+
+ Args:
+ obj: db.Model, model object
+
+ Returns:
+ string, the serialized field value
+ """
+ value = self._get_val_from_obj(obj)
+ return self.get_prep_value(value)
diff --git a/oauth2client/contrib/django_util/signals.py b/oauth2client/contrib/django_util/signals.py
new file mode 100644
index 0000000..e9356b4
--- /dev/null
+++ b/oauth2client/contrib/django_util/signals.py
@@ -0,0 +1,28 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Signals for Google OAuth2 Helper.
+
+This module contains signals for Google OAuth2 Helper. Currently it only
+contains one, which fires when an OAuth2 authorization flow has completed.
+"""
+
+import django.dispatch
+
+"""Signal that fires when OAuth2 Flow has completed.
+It passes the Django request object and the OAuth2 credentials object to the
+ receiver.
+"""
+oauth2_authorized = django.dispatch.Signal(
+ providing_args=["request", "credentials"])
diff --git a/oauth2client/contrib/django_util/site.py b/oauth2client/contrib/django_util/site.py
new file mode 100644
index 0000000..631f79b
--- /dev/null
+++ b/oauth2client/contrib/django_util/site.py
@@ -0,0 +1,26 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Contains Django URL patterns used for OAuth2 flow."""
+
+from django.conf import urls
+
+from oauth2client.contrib.django_util import views
+
+urlpatterns = [
+ urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
+ urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
+]
+
+urls = (urlpatterns, "google_oauth", "google_oauth")
diff --git a/oauth2client/contrib/django_util/storage.py b/oauth2client/contrib/django_util/storage.py
new file mode 100644
index 0000000..5682919
--- /dev/null
+++ b/oauth2client/contrib/django_util/storage.py
@@ -0,0 +1,81 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Contains a storage module that stores credentials using the Django ORM."""
+
+from oauth2client import client
+
+
+class DjangoORMStorage(client.Storage):
+ """Store and retrieve a single credential to and from the Django datastore.
+
+ This Storage helper presumes the Credentials
+ have been stored as a CredentialsField
+ on a db model class.
+ """
+
+ def __init__(self, model_class, key_name, key_value, property_name):
+ """Constructor for Storage.
+
+ Args:
+ model: string, fully qualified name of db.Model model class.
+ key_name: string, key name for the entity that has the credentials
+ key_value: string, key value for the entity that has the
+ credentials.
+ property_name: string, name of the property that is an
+ CredentialsProperty.
+ """
+ super(DjangoORMStorage, self).__init__()
+ self.model_class = model_class
+ self.key_name = key_name
+ self.key_value = key_value
+ self.property_name = property_name
+
+ def locked_get(self):
+ """Retrieve stored credential from the Django ORM.
+
+ Returns:
+ oauth2client.Credentials retrieved from the Django ORM, associated
+ with the ``model``, ``key_value``->``key_name`` pair used to query
+ for the model, and ``property_name`` identifying the
+ ``CredentialsProperty`` field, all of which are defined in the
+ constructor for this Storage object.
+
+ """
+ query = {self.key_name: self.key_value}
+ entities = self.model_class.objects.filter(**query)
+ if len(entities) > 0:
+ credential = getattr(entities[0], self.property_name)
+ if getattr(credential, 'set_store', None) is not None:
+ credential.set_store(self)
+ return credential
+ else:
+ return None
+
+ def locked_put(self, credentials):
+ """Write a Credentials to the Django datastore.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ entity, _ = self.model_class.objects.get_or_create(
+ **{self.key_name: self.key_value})
+
+ setattr(entity, self.property_name, credentials)
+ entity.save()
+
+ def locked_delete(self):
+ """Delete Credentials from the datastore."""
+ query = {self.key_name: self.key_value}
+ self.model_class.objects.filter(**query).delete()
diff --git a/oauth2client/contrib/django_util/views.py b/oauth2client/contrib/django_util/views.py
new file mode 100644
index 0000000..4d8ae03
--- /dev/null
+++ b/oauth2client/contrib/django_util/views.py
@@ -0,0 +1,190 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module contains the views used by the OAuth2 flows.
+
+Their are two views used by the OAuth2 flow, the authorize and the callback
+view. The authorize view kicks off the three-legged OAuth flow, and the
+callback view validates the flow and if successful stores the credentials
+in the configured storage."""
+
+import hashlib
+import json
+import os
+import pickle
+
+from django import http
+from django import shortcuts
+from django.conf import settings
+from django.core import urlresolvers
+from django.shortcuts import redirect
+from six.moves.urllib import parse
+
+from oauth2client import client
+from oauth2client.contrib import django_util
+from oauth2client.contrib.django_util import get_storage
+from oauth2client.contrib.django_util import signals
+
+_CSRF_KEY = 'google_oauth2_csrf_token'
+_FLOW_KEY = 'google_oauth2_flow_{0}'
+
+
+def _make_flow(request, scopes, return_url=None):
+ """Creates a Web Server Flow
+
+ Args:
+ request: A Django request object.
+ scopes: the request oauth2 scopes.
+ return_url: The URL to return to after the flow is complete. Defaults
+ to the path of the current request.
+
+ Returns:
+ An OAuth2 flow object that has been stored in the session.
+ """
+ # Generate a CSRF token to prevent malicious requests.
+ csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
+
+ request.session[_CSRF_KEY] = csrf_token
+
+ state = json.dumps({
+ 'csrf_token': csrf_token,
+ 'return_url': return_url,
+ })
+
+ flow = client.OAuth2WebServerFlow(
+ client_id=django_util.oauth2_settings.client_id,
+ client_secret=django_util.oauth2_settings.client_secret,
+ scope=scopes,
+ state=state,
+ redirect_uri=request.build_absolute_uri(
+ urlresolvers.reverse("google_oauth:callback")))
+
+ flow_key = _FLOW_KEY.format(csrf_token)
+ request.session[flow_key] = pickle.dumps(flow)
+ return flow
+
+
+def _get_flow_for_token(csrf_token, request):
+ """ Looks up the flow in session to recover information about requested
+ scopes.
+
+ Args:
+ csrf_token: The token passed in the callback request that should
+ match the one previously generated and stored in the request on the
+ initial authorization view.
+
+ Returns:
+ The OAuth2 Flow object associated with this flow based on the
+ CSRF token.
+ """
+ flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
+ return None if flow_pickle is None else pickle.loads(flow_pickle)
+
+
+def oauth2_callback(request):
+ """ View that handles the user's return from OAuth2 provider.
+
+ This view verifies the CSRF state and OAuth authorization code, and on
+ success stores the credentials obtained in the storage provider,
+ and redirects to the return_url specified in the authorize view and
+ stored in the session.
+
+ Args:
+ request: Django request.
+
+ Returns:
+ A redirect response back to the return_url.
+ """
+ if 'error' in request.GET:
+ reason = request.GET.get(
+ 'error_description', request.GET.get('error', ''))
+ return http.HttpResponseBadRequest(
+ 'Authorization failed {0}'.format(reason))
+
+ try:
+ encoded_state = request.GET['state']
+ code = request.GET['code']
+ except KeyError:
+ return http.HttpResponseBadRequest(
+ 'Request missing state or authorization code')
+
+ try:
+ server_csrf = request.session[_CSRF_KEY]
+ except KeyError:
+ return http.HttpResponseBadRequest(
+ 'No existing session for this flow.')
+
+ try:
+ state = json.loads(encoded_state)
+ client_csrf = state['csrf_token']
+ return_url = state['return_url']
+ except (ValueError, KeyError):
+ return http.HttpResponseBadRequest('Invalid state parameter.')
+
+ if client_csrf != server_csrf:
+ return http.HttpResponseBadRequest('Invalid CSRF token.')
+
+ flow = _get_flow_for_token(client_csrf, request)
+
+ if not flow:
+ return http.HttpResponseBadRequest('Missing Oauth2 flow.')
+
+ try:
+ credentials = flow.step2_exchange(code)
+ except client.FlowExchangeError as exchange_error:
+ return http.HttpResponseBadRequest(
+ 'An error has occurred: {0}'.format(exchange_error))
+
+ get_storage(request).put(credentials)
+
+ signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
+ request=request, credentials=credentials)
+
+ return shortcuts.redirect(return_url)
+
+
+def oauth2_authorize(request):
+ """ View to start the OAuth2 Authorization flow.
+
+ This view starts the OAuth2 authorization flow. If scopes is passed in
+ as a GET URL parameter, it will authorize those scopes, otherwise the
+ default scopes specified in settings. The return_url can also be
+ specified as a GET parameter, otherwise the referer header will be
+ checked, and if that isn't found it will return to the root path.
+
+ Args:
+ request: The Django request object.
+
+ Returns:
+ A redirect to Google OAuth2 Authorization.
+ """
+ return_url = request.GET.get('return_url', None)
+
+ # Model storage (but not session storage) requires a logged in user
+ if django_util.oauth2_settings.storage_model:
+ if not request.user.is_authenticated():
+ return redirect('{0}?next={1}'.format(
+ settings.LOGIN_URL, parse.quote(request.get_full_path())))
+ # This checks for the case where we ended up here because of a logged
+ # out user but we had credentials for it in the first place
+ elif get_storage(request).get() is not None:
+ return redirect(return_url)
+
+ scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
+
+ if not return_url:
+ return_url = request.META.get('HTTP_REFERER', '/')
+ flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
+ auth_url = flow.step1_get_authorize_url()
+ return shortcuts.redirect(auth_url)
diff --git a/oauth2client/contrib/flask_util.py b/oauth2client/contrib/flask_util.py
new file mode 100644
index 0000000..47c3df1
--- /dev/null
+++ b/oauth2client/contrib/flask_util.py
@@ -0,0 +1,556 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for the Flask web framework
+
+Provides a Flask extension that makes using OAuth2 web server flow easier.
+The extension includes views that handle the entire auth flow and a
+``@required`` decorator to automatically ensure that user credentials are
+available.
+
+
+Configuration
+=============
+
+To configure, you'll need a set of OAuth2 web application credentials from the
+`Google Developer's Console <https://console.developers.google.com/project/_/\
+apiui/credential>`__.
+
+.. code-block:: python
+
+ from oauth2client.contrib.flask_util import UserOAuth2
+
+ app = Flask(__name__)
+
+ app.config['SECRET_KEY'] = 'your-secret-key'
+
+ app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json'
+
+ # or, specify the client id and secret separately
+ app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
+ app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
+
+ oauth2 = UserOAuth2(app)
+
+
+Usage
+=====
+
+Once configured, you can use the :meth:`UserOAuth2.required` decorator to
+ensure that credentials are available within a view.
+
+.. code-block:: python
+ :emphasize-lines: 3,7,10
+
+ # Note that app.route should be the outermost decorator.
+ @app.route('/needs_credentials')
+ @oauth2.required
+ def example():
+ # http is authorized with the user's credentials and can be used
+ # to make http calls.
+ http = oauth2.http()
+
+ # Or, you can access the credentials directly
+ credentials = oauth2.credentials
+
+If you want credentials to be optional for a view, you can leave the decorator
+off and use :meth:`UserOAuth2.has_credentials` to check.
+
+.. code-block:: python
+ :emphasize-lines: 3
+
+ @app.route('/optional')
+ def optional():
+ if oauth2.has_credentials():
+ return 'Credentials found!'
+ else:
+ return 'No credentials!'
+
+
+When credentials are available, you can use :attr:`UserOAuth2.email` and
+:attr:`UserOAuth2.user_id` to access information from the `ID Token
+<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, if
+available.
+
+.. code-block:: python
+ :emphasize-lines: 4
+
+ @app.route('/info')
+ @oauth2.required
+ def info():
+ return "Hello, {} ({})".format(oauth2.email, oauth2.user_id)
+
+
+URLs & Trigging Authorization
+=============================
+
+The extension will add two new routes to your application:
+
+ * ``"oauth2.authorize"`` -> ``/oauth2authorize``
+ * ``"oauth2.callback"`` -> ``/oauth2callback``
+
+When configuring your OAuth2 credentials on the Google Developer's Console, be
+sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized
+callback url.
+
+Typically you don't not need to use these routes directly, just be sure to
+decorate any views that require credentials with ``@oauth2.required``. If
+needed, you can trigger authorization at any time by redirecting the user
+to the URL returned by :meth:`UserOAuth2.authorize_url`.
+
+.. code-block:: python
+ :emphasize-lines: 3
+
+ @app.route('/login')
+ def login():
+ return oauth2.authorize_url("/")
+
+
+Incremental Auth
+================
+
+This extension also supports `Incremental Auth <https://developers.google.com\
+/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it,
+configure the extension with ``include_granted_scopes``.
+
+.. code-block:: python
+
+ oauth2 = UserOAuth2(app, include_granted_scopes=True)
+
+Then specify any additional scopes needed on the decorator, for example:
+
+.. code-block:: python
+ :emphasize-lines: 2,7
+
+ @app.route('/drive')
+ @oauth2.required(scopes=["https://www.googleapis.com/auth/drive"])
+ def requires_drive():
+ ...
+
+ @app.route('/calendar')
+ @oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"])
+ def requires_calendar():
+ ...
+
+The decorator will ensure that the the user has authorized all specified scopes
+before allowing them to access the view, and will also ensure that credentials
+do not lose any previously authorized scopes.
+
+
+Storage
+=======
+
+By default, the extension uses a Flask session-based storage solution. This
+means that credentials are only available for the duration of a session. It
+also means that with Flask's default configuration, the credentials will be
+visible in the session cookie. It's highly recommended to use database-backed
+session and to use https whenever handling user credentials.
+
+If you need the credentials to be available longer than a user session or
+available outside of a request context, you will need to implement your own
+:class:`oauth2client.Storage`.
+"""
+
+from functools import wraps
+import hashlib
+import json
+import os
+import pickle
+
+try:
+ from flask import Blueprint
+ from flask import _app_ctx_stack
+ from flask import current_app
+ from flask import redirect
+ from flask import request
+ from flask import session
+ from flask import url_for
+except ImportError: # pragma: NO COVER
+ raise ImportError('The flask utilities require flask 0.9 or newer.')
+
+import httplib2
+import six.moves.http_client as httplib
+
+from oauth2client import client
+from oauth2client import clientsecrets
+from oauth2client.contrib import dictionary_storage
+
+
+__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
+
+_DEFAULT_SCOPES = ('email',)
+_CREDENTIALS_KEY = 'google_oauth2_credentials'
+_FLOW_KEY = 'google_oauth2_flow_{0}'
+_CSRF_KEY = 'google_oauth2_csrf_token'
+
+
+def _get_flow_for_token(csrf_token):
+ """Retrieves the flow instance associated with a given CSRF token from
+ the Flask session."""
+ flow_pickle = session.pop(
+ _FLOW_KEY.format(csrf_token), None)
+
+ if flow_pickle is None:
+ return None
+ else:
+ return pickle.loads(flow_pickle)
+
+
+class UserOAuth2(object):
+ """Flask extension for making OAuth 2.0 easier.
+
+ Configuration values:
+
+ * ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json
+ file, obtained from the credentials screen in the Google Developers
+ console.
+ * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
+ is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not
+ specified.
+ * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
+ secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE``
+ is not specified.
+
+ If app is specified, all arguments will be passed along to init_app.
+
+ If no app is specified, then you should call init_app in your application
+ factory to finish initialization.
+ """
+
+ def __init__(self, app=None, *args, **kwargs):
+ self.app = app
+ if app is not None:
+ self.init_app(app, *args, **kwargs)
+
+ def init_app(self, app, scopes=None, client_secrets_file=None,
+ client_id=None, client_secret=None, authorize_callback=None,
+ storage=None, **kwargs):
+ """Initialize this extension for the given app.
+
+ Arguments:
+ app: A Flask application.
+ scopes: Optional list of scopes to authorize.
+ client_secrets_file: Path to a file containing client secrets. You
+ can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config
+ value.
+ client_id: If not specifying a client secrets file, specify the
+ OAuth2 client id. You can also specify the
+ GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
+ client secret.
+ client_secret: The OAuth2 client secret. You can also specify the
+ GOOGLE_OAUTH2_CLIENT_SECRET config value.
+ authorize_callback: A function that is executed after successful
+ user authorization.
+ storage: A oauth2client.client.Storage subclass for storing the
+ credentials. By default, this is a Flask session based storage.
+ kwargs: Any additional args are passed along to the Flow
+ constructor.
+ """
+ self.app = app
+ self.authorize_callback = authorize_callback
+ self.flow_kwargs = kwargs
+
+ if storage is None:
+ storage = dictionary_storage.DictionaryStorage(
+ session, key=_CREDENTIALS_KEY)
+ self.storage = storage
+
+ if scopes is None:
+ scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
+ self.scopes = scopes
+
+ self._load_config(client_secrets_file, client_id, client_secret)
+
+ app.register_blueprint(self._create_blueprint())
+
+ def _load_config(self, client_secrets_file, client_id, client_secret):
+ """Loads oauth2 configuration in order of priority.
+
+ Priority:
+ 1. Config passed to the constructor or init_app.
+ 2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
+ config.
+ 3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
+ GOOGLE_OAUTH2_CLIENT_SECRET app config.
+
+ Raises:
+ ValueError if no config could be found.
+ """
+ if client_id and client_secret:
+ self.client_id, self.client_secret = client_id, client_secret
+ return
+
+ if client_secrets_file:
+ self._load_client_secrets(client_secrets_file)
+ return
+
+ if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
+ self._load_client_secrets(
+ self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
+ return
+
+ try:
+ self.client_id, self.client_secret = (
+ self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
+ self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
+ except KeyError:
+ raise ValueError(
+ 'OAuth2 configuration could not be found. Either specify the '
+ 'client_secrets_file or client_id and client_secret or set '
+ 'the app configuration variables '
+ 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
+ 'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
+
+ def _load_client_secrets(self, filename):
+ """Loads client secrets from the given filename."""
+ client_type, client_info = clientsecrets.loadfile(filename)
+ if client_type != clientsecrets.TYPE_WEB:
+ raise ValueError(
+ 'The flow specified in {0} is not supported.'.format(
+ client_type))
+
+ self.client_id = client_info['client_id']
+ self.client_secret = client_info['client_secret']
+
+ def _make_flow(self, return_url=None, **kwargs):
+ """Creates a Web Server Flow"""
+ # Generate a CSRF token to prevent malicious requests.
+ csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
+
+ session[_CSRF_KEY] = csrf_token
+
+ state = json.dumps({
+ 'csrf_token': csrf_token,
+ 'return_url': return_url
+ })
+
+ kw = self.flow_kwargs.copy()
+ kw.update(kwargs)
+
+ extra_scopes = kw.pop('scopes', [])
+ scopes = set(self.scopes).union(set(extra_scopes))
+
+ flow = client.OAuth2WebServerFlow(
+ client_id=self.client_id,
+ client_secret=self.client_secret,
+ scope=scopes,
+ state=state,
+ redirect_uri=url_for('oauth2.callback', _external=True),
+ **kw)
+
+ flow_key = _FLOW_KEY.format(csrf_token)
+ session[flow_key] = pickle.dumps(flow)
+
+ return flow
+
+ def _create_blueprint(self):
+ bp = Blueprint('oauth2', __name__)
+ bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
+ bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
+
+ return bp
+
+ def authorize_view(self):
+ """Flask view that starts the authorization flow.
+
+ Starts flow by redirecting the user to the OAuth2 provider.
+ """
+ args = request.args.to_dict()
+
+ # Scopes will be passed as mutliple args, and to_dict() will only
+ # return one. So, we use getlist() to get all of the scopes.
+ args['scopes'] = request.args.getlist('scopes')
+
+ return_url = args.pop('return_url', None)
+ if return_url is None:
+ return_url = request.referrer or '/'
+
+ flow = self._make_flow(return_url=return_url, **args)
+ auth_url = flow.step1_get_authorize_url()
+
+ return redirect(auth_url)
+
+ def callback_view(self):
+ """Flask view that handles the user's return from OAuth2 provider.
+
+ On return, exchanges the authorization code for credentials and stores
+ the credentials.
+ """
+ if 'error' in request.args:
+ reason = request.args.get(
+ 'error_description', request.args.get('error', ''))
+ return ('Authorization failed: {0}'.format(reason),
+ httplib.BAD_REQUEST)
+
+ try:
+ encoded_state = request.args['state']
+ server_csrf = session[_CSRF_KEY]
+ code = request.args['code']
+ except KeyError:
+ return 'Invalid request', httplib.BAD_REQUEST
+
+ try:
+ state = json.loads(encoded_state)
+ client_csrf = state['csrf_token']
+ return_url = state['return_url']
+ except (ValueError, KeyError):
+ return 'Invalid request state', httplib.BAD_REQUEST
+
+ if client_csrf != server_csrf:
+ return 'Invalid request state', httplib.BAD_REQUEST
+
+ flow = _get_flow_for_token(server_csrf)
+
+ if flow is None:
+ return 'Invalid request state', httplib.BAD_REQUEST
+
+ # Exchange the auth code for credentials.
+ try:
+ credentials = flow.step2_exchange(code)
+ except client.FlowExchangeError as exchange_error:
+ current_app.logger.exception(exchange_error)
+ content = 'An error occurred: {0}'.format(exchange_error)
+ return content, httplib.BAD_REQUEST
+
+ # Save the credentials to the storage.
+ self.storage.put(credentials)
+
+ if self.authorize_callback:
+ self.authorize_callback(credentials)
+
+ return redirect(return_url)
+
+ @property
+ def credentials(self):
+ """The credentials for the current user or None if unavailable."""
+ ctx = _app_ctx_stack.top
+
+ if not hasattr(ctx, _CREDENTIALS_KEY):
+ ctx.google_oauth2_credentials = self.storage.get()
+
+ return ctx.google_oauth2_credentials
+
+ def has_credentials(self):
+ """Returns True if there are valid credentials for the current user."""
+ if not self.credentials:
+ return False
+ # Is the access token expired? If so, do we have an refresh token?
+ elif (self.credentials.access_token_expired and
+ not self.credentials.refresh_token):
+ return False
+ else:
+ return True
+
+ @property
+ def email(self):
+ """Returns the user's email address or None if there are no credentials.
+
+ The email address is provided by the current credentials' id_token.
+ This should not be used as unique identifier as the user can change
+ their email. If you need a unique identifier, use user_id.
+ """
+ if not self.credentials:
+ return None
+ try:
+ return self.credentials.id_token['email']
+ except KeyError:
+ current_app.logger.error(
+ 'Invalid id_token {0}'.format(self.credentials.id_token))
+
+ @property
+ def user_id(self):
+ """Returns the a unique identifier for the user
+
+ Returns None if there are no credentials.
+
+ The id is provided by the current credentials' id_token.
+ """
+ if not self.credentials:
+ return None
+ try:
+ return self.credentials.id_token['sub']
+ except KeyError:
+ current_app.logger.error(
+ 'Invalid id_token {0}'.format(self.credentials.id_token))
+
+ def authorize_url(self, return_url, **kwargs):
+ """Creates a URL that can be used to start the authorization flow.
+
+ When the user is directed to the URL, the authorization flow will
+ begin. Once complete, the user will be redirected to the specified
+ return URL.
+
+ Any kwargs are passed into the flow constructor.
+ """
+ return url_for('oauth2.authorize', return_url=return_url, **kwargs)
+
+ def required(self, decorated_function=None, scopes=None,
+ **decorator_kwargs):
+ """Decorator to require OAuth2 credentials for a view.
+
+ If credentials are not available for the current user, then they will
+ be redirected to the authorization flow. Once complete, the user will
+ be redirected back to the original page.
+ """
+
+ def curry_wrapper(wrapped_function):
+ @wraps(wrapped_function)
+ def required_wrapper(*args, **kwargs):
+ return_url = decorator_kwargs.pop('return_url', request.url)
+
+ requested_scopes = set(self.scopes)
+ if scopes is not None:
+ requested_scopes |= set(scopes)
+ if self.has_credentials():
+ requested_scopes |= self.credentials.scopes
+
+ requested_scopes = list(requested_scopes)
+
+ # Does the user have credentials and does the credentials have
+ # all of the needed scopes?
+ if (self.has_credentials() and
+ self.credentials.has_scopes(requested_scopes)):
+ return wrapped_function(*args, **kwargs)
+ # Otherwise, redirect to authorization
+ else:
+ auth_url = self.authorize_url(
+ return_url,
+ scopes=requested_scopes,
+ **decorator_kwargs)
+
+ return redirect(auth_url)
+
+ return required_wrapper
+
+ if decorated_function:
+ return curry_wrapper(decorated_function)
+ else:
+ return curry_wrapper
+
+ def http(self, *args, **kwargs):
+ """Returns an authorized http instance.
+
+ Can only be called if there are valid credentials for the user, such
+ as inside of a view that is decorated with @required.
+
+ Args:
+ *args: Positional arguments passed to httplib2.Http constructor.
+ **kwargs: Positional arguments passed to httplib2.Http constructor.
+
+ Raises:
+ ValueError if no credentials are available.
+ """
+ if not self.credentials:
+ raise ValueError('No credentials available.')
+ return self.credentials.authorize(httplib2.Http(*args, **kwargs))
diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py
new file mode 100644
index 0000000..f3a6ca1
--- /dev/null
+++ b/oauth2client/contrib/gce.py
@@ -0,0 +1,162 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for Google Compute Engine
+
+Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
+"""
+
+import logging
+import warnings
+
+import httplib2
+
+from oauth2client import client
+from oauth2client.contrib import _metadata
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+logger = logging.getLogger(__name__)
+
+_SCOPES_WARNING = """\
+You have requested explicit scopes to be used with a GCE service account.
+Using this argument will have no effect on the actual scopes for tokens
+requested. These scopes are set at VM instance creation time and
+can't be overridden in the request.
+"""
+
+
+class AppAssertionCredentials(client.AssertionCredentials):
+ """Credentials object for Compute Engine Assertion Grants
+
+ This object will allow a Compute Engine instance to identify itself to
+ Google and other OAuth 2.0 servers that can verify assertions. It can be
+ used for the purpose of accessing data stored under an account assigned to
+ the Compute Engine instance itself.
+
+ This credential does not require a flow to instantiate because it
+ represents a two legged flow, and therefore has all of the required
+ information to generate and refresh its own access tokens.
+
+ Note that :attr:`service_account_email` and :attr:`scopes`
+ will both return None until the credentials have been refreshed.
+ To check whether credentials have previously been refreshed use
+ :attr:`invalid`.
+ """
+
+ def __init__(self, email=None, *args, **kwargs):
+ """Constructor for AppAssertionCredentials
+
+ Args:
+ email: an email that specifies the service account to use.
+ Only necessary if using custom service accounts
+ (see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount).
+ """
+ if 'scopes' in kwargs:
+ warnings.warn(_SCOPES_WARNING)
+ kwargs['scopes'] = None
+
+ # Assertion type is no longer used, but still in the
+ # parent class signature.
+ super(AppAssertionCredentials, self).__init__(None, *args, **kwargs)
+
+ self.service_account_email = email
+ self.scopes = None
+ self.invalid = True
+
+ @classmethod
+ def from_json(cls, json_data):
+ raise NotImplementedError(
+ 'Cannot serialize credentials for GCE service accounts.')
+
+ def to_json(self):
+ raise NotImplementedError(
+ 'Cannot serialize credentials for GCE service accounts.')
+
+ def retrieve_scopes(self, http):
+ """Retrieves the canonical list of scopes for this access token.
+
+ Overrides client.Credentials.retrieve_scopes. Fetches scopes info
+ from the metadata server.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+
+ Returns:
+ A set of strings containing the canonical list of scopes.
+ """
+ self._retrieve_info(http.request)
+ return self.scopes
+
+ def _retrieve_info(self, http_request):
+ """Validates invalid service accounts by retrieving service account info.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make the
+ request to the metadata server
+ """
+ if self.invalid:
+ info = _metadata.get_service_account_info(
+ http_request,
+ service_account=self.service_account_email or 'default')
+ self.invalid = False
+ self.service_account_email = info['email']
+ self.scopes = info['scopes']
+
+ def _refresh(self, http_request):
+ """Refreshes the access_token.
+
+ Skip all the storage hoops and just refresh using the API.
+
+ Args:
+ http_request: callable, a callable that matches the method
+ signature of httplib2.Http.request, used to make
+ the refresh request.
+
+ Raises:
+ HttpAccessTokenRefreshError: When the refresh fails.
+ """
+ try:
+ self._retrieve_info(http_request)
+ self.access_token, self.token_expiry = _metadata.get_token(
+ http_request, service_account=self.service_account_email)
+ except httplib2.HttpLib2Error as e:
+ raise client.HttpAccessTokenRefreshError(str(e))
+
+ @property
+ def serialization_data(self):
+ raise NotImplementedError(
+ 'Cannot serialize credentials for GCE service accounts.')
+
+ def create_scoped_required(self):
+ return False
+
+ def sign_blob(self, blob):
+ """Cryptographically sign a blob (of bytes).
+
+ This method is provided to support a common interface, but
+ the actual key used for a Google Compute Engine service account
+ is not available, so it can't be used to sign content.
+
+ Args:
+ blob: bytes, Message to be signed.
+
+ Raises:
+ NotImplementedError, always.
+ """
+ raise NotImplementedError(
+ 'Compute Engine service accounts cannot sign blobs')
diff --git a/oauth2client/contrib/keyring_storage.py b/oauth2client/contrib/keyring_storage.py
new file mode 100644
index 0000000..f4f2e30
--- /dev/null
+++ b/oauth2client/contrib/keyring_storage.py
@@ -0,0 +1,98 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A keyring based Storage.
+
+A Storage for Credentials that uses the keyring module.
+"""
+
+import threading
+
+import keyring
+
+from oauth2client import client
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class Storage(client.Storage):
+ """Store and retrieve a single credential to and from the keyring.
+
+ To use this module you must have the keyring module installed. See
+ <http://pypi.python.org/pypi/keyring/>. This is an optional module and is
+ not installed with oauth2client by default because it does not work on all
+ the platforms that oauth2client supports, such as Google App Engine.
+
+ The keyring module <http://pypi.python.org/pypi/keyring/> is a
+ cross-platform library for access the keyring capabilities of the local
+ system. The user will be prompted for their keyring password when this
+ module is used, and the manner in which the user is prompted will vary per
+ platform.
+
+ Usage::
+
+ from oauth2client import keyring_storage
+
+ s = keyring_storage.Storage('name_of_application', 'user1')
+ credentials = s.get()
+
+ """
+
+ def __init__(self, service_name, user_name):
+ """Constructor.
+
+ Args:
+ service_name: string, The name of the service under which the
+ credentials are stored.
+ user_name: string, The name of the user to store credentials for.
+ """
+ super(Storage, self).__init__(lock=threading.Lock())
+ self._service_name = service_name
+ self._user_name = user_name
+
+ def locked_get(self):
+ """Retrieve Credential from file.
+
+ Returns:
+ oauth2client.client.Credentials
+ """
+ credentials = None
+ content = keyring.get_password(self._service_name, self._user_name)
+
+ if content is not None:
+ try:
+ credentials = client.Credentials.new_from_json(content)
+ credentials.set_store(self)
+ except ValueError:
+ pass
+
+ return credentials
+
+ def locked_put(self, credentials):
+ """Write Credentials to file.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ keyring.set_password(self._service_name, self._user_name,
+ credentials.to_json())
+
+ def locked_delete(self):
+ """Delete Credentials file.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ keyring.set_password(self._service_name, self._user_name, '')
diff --git a/oauth2client/contrib/locked_file.py b/oauth2client/contrib/locked_file.py
new file mode 100644
index 0000000..0d28ebb
--- /dev/null
+++ b/oauth2client/contrib/locked_file.py
@@ -0,0 +1,234 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Locked file interface that should work on Unix and Windows pythons.
+
+This module first tries to use fcntl locking to ensure serialized access
+to a file, then falls back on a lock file if that is unavialable.
+
+Usage::
+
+ f = LockedFile('filename', 'r+b', 'rb')
+ f.open_and_lock()
+ if f.is_locked():
+ print('Acquired filename with r+b mode')
+ f.file_handle().write('locked data')
+ else:
+ print('Acquired filename with rb mode')
+ f.unlock_and_close()
+
+"""
+
+from __future__ import print_function
+
+import errno
+import logging
+import os
+import time
+
+from oauth2client import util
+
+
+__author__ = 'cache@google.com (David T McWherter)'
+
+logger = logging.getLogger(__name__)
+
+
+class CredentialsFileSymbolicLinkError(Exception):
+ """Credentials files must not be symbolic links."""
+
+
+class AlreadyLockedException(Exception):
+ """Trying to lock a file that has already been locked by the LockedFile."""
+ pass
+
+
+def validate_file(filename):
+ if os.path.islink(filename):
+ raise CredentialsFileSymbolicLinkError(
+ 'File: {0} is a symbolic link.'.format(filename))
+
+
+class _Opener(object):
+ """Base class for different locking primitives."""
+
+ def __init__(self, filename, mode, fallback_mode):
+ """Create an Opener.
+
+ Args:
+ filename: string, The pathname of the file.
+ mode: string, The preferred mode to access the file with.
+ fallback_mode: string, The mode to use if locking fails.
+ """
+ self._locked = False
+ self._filename = filename
+ self._mode = mode
+ self._fallback_mode = fallback_mode
+ self._fh = None
+ self._lock_fd = None
+
+ def is_locked(self):
+ """Was the file locked."""
+ return self._locked
+
+ def file_handle(self):
+ """The file handle to the file. Valid only after opened."""
+ return self._fh
+
+ def filename(self):
+ """The filename that is being locked."""
+ return self._filename
+
+ def open_and_lock(self, timeout, delay):
+ """Open the file and lock it.
+
+ Args:
+ timeout: float, How long to try to lock for.
+ delay: float, How long to wait between retries.
+ """
+ pass
+
+ def unlock_and_close(self):
+ """Unlock and close the file."""
+ pass
+
+
+class _PosixOpener(_Opener):
+ """Lock files using Posix advisory lock files."""
+
+ def open_and_lock(self, timeout, delay):
+ """Open the file and lock it.
+
+ Tries to create a .lock file next to the file we're trying to open.
+
+ Args:
+ timeout: float, How long to try to lock for.
+ delay: float, How long to wait between retries.
+
+ Raises:
+ AlreadyLockedException: if the lock is already acquired.
+ IOError: if the open fails.
+ CredentialsFileSymbolicLinkError if the file is a symbolic link.
+ """
+ if self._locked:
+ raise AlreadyLockedException(
+ 'File {0} is already locked'.format(self._filename))
+ self._locked = False
+
+ validate_file(self._filename)
+ try:
+ self._fh = open(self._filename, self._mode)
+ except IOError as e:
+ # If we can't access with _mode, try _fallback_mode and don't lock.
+ if e.errno == errno.EACCES:
+ self._fh = open(self._filename, self._fallback_mode)
+ return
+
+ lock_filename = self._posix_lockfile(self._filename)
+ start_time = time.time()
+ while True:
+ try:
+ self._lock_fd = os.open(lock_filename,
+ os.O_CREAT | os.O_EXCL | os.O_RDWR)
+ self._locked = True
+ break
+
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+ if (time.time() - start_time) >= timeout:
+ logger.warn('Could not acquire lock %s in %s seconds',
+ lock_filename, timeout)
+ # Close the file and open in fallback_mode.
+ if self._fh:
+ self._fh.close()
+ self._fh = open(self._filename, self._fallback_mode)
+ return
+ time.sleep(delay)
+
+ def unlock_and_close(self):
+ """Unlock a file by removing the .lock file, and close the handle."""
+ if self._locked:
+ lock_filename = self._posix_lockfile(self._filename)
+ os.close(self._lock_fd)
+ os.unlink(lock_filename)
+ self._locked = False
+ self._lock_fd = None
+ if self._fh:
+ self._fh.close()
+
+ def _posix_lockfile(self, filename):
+ """The name of the lock file to use for posix locking."""
+ return '{0}.lock'.format(filename)
+
+
+class LockedFile(object):
+ """Represent a file that has exclusive access."""
+
+ @util.positional(4)
+ def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
+ """Construct a LockedFile.
+
+ Args:
+ filename: string, The path of the file to open.
+ mode: string, The mode to try to open the file with.
+ fallback_mode: string, The mode to use if locking fails.
+ use_native_locking: bool, Whether or not fcntl/win32 locking is
+ used.
+ """
+ opener = None
+ if not opener and use_native_locking:
+ try:
+ from oauth2client.contrib._win32_opener import _Win32Opener
+ opener = _Win32Opener(filename, mode, fallback_mode)
+ except ImportError:
+ try:
+ from oauth2client.contrib._fcntl_opener import _FcntlOpener
+ opener = _FcntlOpener(filename, mode, fallback_mode)
+ except ImportError:
+ pass
+
+ if not opener:
+ opener = _PosixOpener(filename, mode, fallback_mode)
+
+ self._opener = opener
+
+ def filename(self):
+ """Return the filename we were constructed with."""
+ return self._opener._filename
+
+ def file_handle(self):
+ """Return the file_handle to the opened file."""
+ return self._opener.file_handle()
+
+ def is_locked(self):
+ """Return whether we successfully locked the file."""
+ return self._opener.is_locked()
+
+ def open_and_lock(self, timeout=0, delay=0.05):
+ """Open the file, trying to lock it.
+
+ Args:
+ timeout: float, The number of seconds to try to acquire the lock.
+ delay: float, The number of seconds to wait between retry attempts.
+
+ Raises:
+ AlreadyLockedException: if the lock is already acquired.
+ IOError: if the open fails.
+ """
+ self._opener.open_and_lock(timeout, delay)
+
+ def unlock_and_close(self):
+ """Unlock and close a file."""
+ self._opener.unlock_and_close()
diff --git a/oauth2client/contrib/multiprocess_file_storage.py b/oauth2client/contrib/multiprocess_file_storage.py
new file mode 100644
index 0000000..e9e8c8c
--- /dev/null
+++ b/oauth2client/contrib/multiprocess_file_storage.py
@@ -0,0 +1,355 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Multiprocess file credential storage.
+
+This module provides file-based storage that supports multiple credentials and
+cross-thread and process access.
+
+This module supersedes the functionality previously found in `multistore_file`.
+
+This module provides :class:`MultiprocessFileStorage` which:
+ * Is tied to a single credential via a user-specified key. This key can be
+ used to distinguish between multiple users, client ids, and/or scopes.
+ * Can be safely accessed and refreshed across threads and processes.
+
+Process & thread safety guarantees the following behavior:
+ * If one thread or process refreshes a credential, subsequent refreshes
+ from other processes will re-fetch the credentials from the file instead
+ of performing an http request.
+ * If two processes or threads attempt to refresh concurrently, only one
+ will be able to acquire the lock and refresh, with the deadlock caveat
+ below.
+ * The interprocess lock will not deadlock, instead, the if a process can
+ not acquire the interprocess lock within ``INTERPROCESS_LOCK_DEADLINE``
+ it will allow refreshing the credential but will not write the updated
+ credential to disk, This logic happens during every lock cycle - if the
+ credentials are refreshed again it will retry locking and writing as
+ normal.
+
+Usage
+=====
+
+Before using the storage, you need to decide how you want to key the
+credentials. A few common strategies include:
+
+ * If you're storing credentials for multiple users in a single file, use
+ a unique identifier for each user as the key.
+ * If you're storing credentials for multiple client IDs in a single file,
+ use the client ID as the key.
+ * If you're storing multiple credentials for one user, use the scopes as
+ the key.
+ * If you have a complicated setup, use a compound key. For example, you
+ can use a combination of the client ID and scopes as the key.
+
+Create an instance of :class:`MultiprocessFileStorage` for each credential you
+want to store, for example::
+
+ filename = 'credentials'
+ key = '{}-{}'.format(client_id, user_id)
+ storage = MultiprocessFileStorage(filename, key)
+
+To store the credentials::
+
+ storage.put(credentials)
+
+If you're going to continue to use the credentials after storing them, be sure
+to call :func:`set_store`::
+
+ credentials.set_store(storage)
+
+To retrieve the credentials::
+
+ storage.get(credentials)
+
+"""
+
+import base64
+import json
+import logging
+import os
+import threading
+
+import fasteners
+from six import iteritems
+
+from oauth2client import _helpers
+from oauth2client import client
+
+
+#: The maximum amount of time, in seconds, to wait when acquire the
+#: interprocess lock before falling back to read-only mode.
+INTERPROCESS_LOCK_DEADLINE = 1
+
+logger = logging.getLogger(__name__)
+_backends = {}
+_backends_lock = threading.Lock()
+
+
+def _create_file_if_needed(filename):
+ """Creates the an empty file if it does not already exist.
+
+ Returns:
+ True if the file was created, False otherwise.
+ """
+ if os.path.exists(filename):
+ return False
+ else:
+ # Equivalent to "touch".
+ open(filename, 'a+b').close()
+ logger.info('Credential file {0} created'.format(filename))
+ return True
+
+
+def _load_credentials_file(credentials_file):
+ """Load credentials from the given file handle.
+
+ The file is expected to be in this format:
+
+ {
+ "file_version": 2,
+ "credentials": {
+ "key": "base64 encoded json representation of credentials."
+ }
+ }
+
+ This function will warn and return empty credentials instead of raising
+ exceptions.
+
+ Args:
+ credentials_file: An open file handle.
+
+ Returns:
+ A dictionary mapping user-defined keys to an instance of
+ :class:`oauth2client.client.Credentials`.
+ """
+ try:
+ credentials_file.seek(0)
+ data = json.load(credentials_file)
+ except Exception:
+ logger.warning(
+ 'Credentials file could not be loaded, will ignore and '
+ 'overwrite.')
+ return {}
+
+ if data.get('file_version') != 2:
+ logger.warning(
+ 'Credentials file is not version 2, will ignore and '
+ 'overwrite.')
+ return {}
+
+ credentials = {}
+
+ for key, encoded_credential in iteritems(data.get('credentials', {})):
+ try:
+ credential_json = base64.b64decode(encoded_credential)
+ credential = client.Credentials.new_from_json(credential_json)
+ credentials[key] = credential
+ except:
+ logger.warning(
+ 'Invalid credential {0} in file, ignoring.'.format(key))
+
+ return credentials
+
+
+def _write_credentials_file(credentials_file, credentials):
+ """Writes credentials to a file.
+
+ Refer to :func:`_load_credentials_file` for the format.
+
+ Args:
+ credentials_file: An open file handle, must be read/write.
+ credentials: A dictionary mapping user-defined keys to an instance of
+ :class:`oauth2client.client.Credentials`.
+ """
+ data = {'file_version': 2, 'credentials': {}}
+
+ for key, credential in iteritems(credentials):
+ credential_json = credential.to_json()
+ encoded_credential = _helpers._from_bytes(base64.b64encode(
+ _helpers._to_bytes(credential_json)))
+ data['credentials'][key] = encoded_credential
+
+ credentials_file.seek(0)
+ json.dump(data, credentials_file)
+ credentials_file.truncate()
+
+
+class _MultiprocessStorageBackend(object):
+ """Thread-local backend for multiprocess storage.
+
+ Each process has only one instance of this backend per file. All threads
+ share a single instance of this backend. This ensures that all threads
+ use the same thread lock and process lock when accessing the file.
+ """
+
+ def __init__(self, filename):
+ self._file = None
+ self._filename = filename
+ self._process_lock = fasteners.InterProcessLock(
+ '{0}.lock'.format(filename))
+ self._thread_lock = threading.Lock()
+ self._read_only = False
+ self._credentials = {}
+
+ def _load_credentials(self):
+ """(Re-)loads the credentials from the file."""
+ if not self._file:
+ return
+
+ loaded_credentials = _load_credentials_file(self._file)
+ self._credentials.update(loaded_credentials)
+
+ logger.debug('Read credential file')
+
+ def _write_credentials(self):
+ if self._read_only:
+ logger.debug('In read-only mode, not writing credentials.')
+ return
+
+ _write_credentials_file(self._file, self._credentials)
+ logger.debug('Wrote credential file {0}.'.format(self._filename))
+
+ def acquire_lock(self):
+ self._thread_lock.acquire()
+ locked = self._process_lock.acquire(timeout=INTERPROCESS_LOCK_DEADLINE)
+
+ if locked:
+ _create_file_if_needed(self._filename)
+ self._file = open(self._filename, 'r+')
+ self._read_only = False
+
+ else:
+ logger.warn(
+ 'Failed to obtain interprocess lock for credentials. '
+ 'If a credential is being refreshed, other processes may '
+ 'not see the updated access token and refresh as well.')
+ if os.path.exists(self._filename):
+ self._file = open(self._filename, 'r')
+ else:
+ self._file = None
+ self._read_only = True
+
+ self._load_credentials()
+
+ def release_lock(self):
+ if self._file is not None:
+ self._file.close()
+ self._file = None
+
+ if not self._read_only:
+ self._process_lock.release()
+
+ self._thread_lock.release()
+
+ def _refresh_predicate(self, credentials):
+ if credentials is None:
+ return True
+ elif credentials.invalid:
+ return True
+ elif credentials.access_token_expired:
+ return True
+ else:
+ return False
+
+ def locked_get(self, key):
+ # Check if the credential is already in memory.
+ credentials = self._credentials.get(key, None)
+
+ # Use the refresh predicate to determine if the entire store should be
+ # reloaded. This basically checks if the credentials are invalid
+ # or expired. This covers the situation where another process has
+ # refreshed the credentials and this process doesn't know about it yet.
+ # In that case, this process won't needlessly refresh the credentials.
+ if self._refresh_predicate(credentials):
+ self._load_credentials()
+ credentials = self._credentials.get(key, None)
+
+ return credentials
+
+ def locked_put(self, key, credentials):
+ self._load_credentials()
+ self._credentials[key] = credentials
+ self._write_credentials()
+
+ def locked_delete(self, key):
+ self._load_credentials()
+ self._credentials.pop(key, None)
+ self._write_credentials()
+
+
+def _get_backend(filename):
+ """A helper method to get or create a backend with thread locking.
+
+ This ensures that only one backend is used per-file per-process, so that
+ thread and process locks are appropriately shared.
+
+ Args:
+ filename: The full path to the credential storage file.
+
+ Returns:
+ An instance of :class:`_MultiprocessStorageBackend`.
+ """
+ filename = os.path.abspath(filename)
+
+ with _backends_lock:
+ if filename not in _backends:
+ _backends[filename] = _MultiprocessStorageBackend(filename)
+ return _backends[filename]
+
+
+class MultiprocessFileStorage(client.Storage):
+ """Multiprocess file credential storage.
+
+ Args:
+ filename: The path to the file where credentials will be stored.
+ key: An arbitrary string used to uniquely identify this set of
+ credentials. For example, you may use the user's ID as the key or
+ a combination of the client ID and user ID.
+ """
+ def __init__(self, filename, key):
+ self._key = key
+ self._backend = _get_backend(filename)
+
+ def acquire_lock(self):
+ self._backend.acquire_lock()
+
+ def release_lock(self):
+ self._backend.release_lock()
+
+ def locked_get(self):
+ """Retrieves the current credentials from the store.
+
+ Returns:
+ An instance of :class:`oauth2client.client.Credentials` or `None`.
+ """
+ credential = self._backend.locked_get(self._key)
+
+ if credential is not None:
+ credential.set_store(self)
+
+ return credential
+
+ def locked_put(self, credentials):
+ """Writes the given credentials to the store.
+
+ Args:
+ credentials: an instance of
+ :class:`oauth2client.client.Credentials`.
+ """
+ return self._backend.locked_put(self._key, credentials)
+
+ def locked_delete(self):
+ """Deletes the current credentials from the store."""
+ return self._backend.locked_delete(self._key)
diff --git a/oauth2client/contrib/multistore_file.py b/oauth2client/contrib/multistore_file.py
new file mode 100644
index 0000000..10f4cb4
--- /dev/null
+++ b/oauth2client/contrib/multistore_file.py
@@ -0,0 +1,505 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Multi-credential file store with lock support.
+
+This module implements a JSON credential store where multiple
+credentials can be stored in one file. That file supports locking
+both in a single process and across processes.
+
+The credential themselves are keyed off of:
+
+* client_id
+* user_agent
+* scope
+
+The format of the stored data is like so::
+
+ {
+ 'file_version': 1,
+ 'data': [
+ {
+ 'key': {
+ 'clientId': '<client id>',
+ 'userAgent': '<user agent>',
+ 'scope': '<scope>'
+ },
+ 'credential': {
+ # JSON serialized Credentials.
+ }
+ }
+ ]
+ }
+
+"""
+
+import errno
+import json
+import logging
+import os
+import threading
+
+from oauth2client import client
+from oauth2client import util
+from oauth2client.contrib import locked_file
+
+__author__ = 'jbeda@google.com (Joe Beda)'
+
+logger = logging.getLogger(__name__)
+
+logger.warning(
+ 'The oauth2client.contrib.multistore_file module has been deprecated and '
+ 'will be removed in the next release of oauth2client. Please migrate to '
+ 'multiprocess_file_storage.')
+
+# A dict from 'filename'->_MultiStore instances
+_multistores = {}
+_multistores_lock = threading.Lock()
+
+
+class Error(Exception):
+ """Base error for this module."""
+
+
+class NewerCredentialStoreError(Error):
+ """The credential store is a newer version than supported."""
+
+
+def _dict_to_tuple_key(dictionary):
+ """Converts a dictionary to a tuple that can be used as an immutable key.
+
+ The resulting key is always sorted so that logically equivalent
+ dictionaries always produce an identical tuple for a key.
+
+ Args:
+ dictionary: the dictionary to use as the key.
+
+ Returns:
+ A tuple representing the dictionary in it's naturally sorted ordering.
+ """
+ return tuple(sorted(dictionary.items()))
+
+
+@util.positional(4)
+def get_credential_storage(filename, client_id, user_agent, scope,
+ warn_on_readonly=True):
+ """Get a Storage instance for a credential.
+
+ Args:
+ filename: The JSON file storing a set of credentials
+ client_id: The client_id for the credential
+ user_agent: The user agent for the credential
+ scope: string or iterable of strings, Scope(s) being requested
+ warn_on_readonly: if True, log a warning if the store is readonly
+
+ Returns:
+ An object derived from client.Storage for getting/setting the
+ credential.
+ """
+ # Recreate the legacy key with these specific parameters
+ key = {'clientId': client_id, 'userAgent': user_agent,
+ 'scope': util.scopes_to_string(scope)}
+ return get_credential_storage_custom_key(
+ filename, key, warn_on_readonly=warn_on_readonly)
+
+
+@util.positional(2)
+def get_credential_storage_custom_string_key(filename, key_string,
+ warn_on_readonly=True):
+ """Get a Storage instance for a credential using a single string as a key.
+
+ Allows you to provide a string as a custom key that will be used for
+ credential storage and retrieval.
+
+ Args:
+ filename: The JSON file storing a set of credentials
+ key_string: A string to use as the key for storing this credential.
+ warn_on_readonly: if True, log a warning if the store is readonly
+
+ Returns:
+ An object derived from client.Storage for getting/setting the
+ credential.
+ """
+ # Create a key dictionary that can be used
+ key_dict = {'key': key_string}
+ return get_credential_storage_custom_key(
+ filename, key_dict, warn_on_readonly=warn_on_readonly)
+
+
+@util.positional(2)
+def get_credential_storage_custom_key(filename, key_dict,
+ warn_on_readonly=True):
+ """Get a Storage instance for a credential using a dictionary as a key.
+
+ Allows you to provide a dictionary as a custom key that will be used for
+ credential storage and retrieval.
+
+ Args:
+ filename: The JSON file storing a set of credentials
+ key_dict: A dictionary to use as the key for storing this credential.
+ There is no ordering of the keys in the dictionary. Logically
+ equivalent dictionaries will produce equivalent storage keys.
+ warn_on_readonly: if True, log a warning if the store is readonly
+
+ Returns:
+ An object derived from client.Storage for getting/setting the
+ credential.
+ """
+ multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
+ key = _dict_to_tuple_key(key_dict)
+ return multistore._get_storage(key)
+
+
+@util.positional(1)
+def get_all_credential_keys(filename, warn_on_readonly=True):
+ """Gets all the registered credential keys in the given Multistore.
+
+ Args:
+ filename: The JSON file storing a set of credentials
+ warn_on_readonly: if True, log a warning if the store is readonly
+
+ Returns:
+ A list of the credential keys present in the file. They are returned
+ as dictionaries that can be passed into
+ get_credential_storage_custom_key to get the actual credentials.
+ """
+ multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
+ multistore._lock()
+ try:
+ return multistore._get_all_credential_keys()
+ finally:
+ multistore._unlock()
+
+
+@util.positional(1)
+def _get_multistore(filename, warn_on_readonly=True):
+ """A helper method to initialize the multistore with proper locking.
+
+ Args:
+ filename: The JSON file storing a set of credentials
+ warn_on_readonly: if True, log a warning if the store is readonly
+
+ Returns:
+ A multistore object
+ """
+ filename = os.path.expanduser(filename)
+ _multistores_lock.acquire()
+ try:
+ multistore = _multistores.setdefault(
+ filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
+ finally:
+ _multistores_lock.release()
+ return multistore
+
+
+class _MultiStore(object):
+ """A file backed store for multiple credentials."""
+
+ @util.positional(2)
+ def __init__(self, filename, warn_on_readonly=True):
+ """Initialize the class.
+
+ This will create the file if necessary.
+ """
+ self._file = locked_file.LockedFile(filename, 'r+', 'r')
+ self._thread_lock = threading.Lock()
+ self._read_only = False
+ self._warn_on_readonly = warn_on_readonly
+
+ self._create_file_if_needed()
+
+ # Cache of deserialized store. This is only valid after the
+ # _MultiStore is locked or _refresh_data_cache is called. This is
+ # of the form of:
+ #
+ # ((key, value), (key, value)...) -> OAuth2Credential
+ #
+ # If this is None, then the store hasn't been read yet.
+ self._data = None
+
+ class _Storage(client.Storage):
+ """A Storage object that can read/write a single credential."""
+
+ def __init__(self, multistore, key):
+ self._multistore = multistore
+ self._key = key
+
+ def acquire_lock(self):
+ """Acquires any lock necessary to access this Storage.
+
+ This lock is not reentrant.
+ """
+ self._multistore._lock()
+
+ def release_lock(self):
+ """Release the Storage lock.
+
+ Trying to release a lock that isn't held will result in a
+ RuntimeError.
+ """
+ self._multistore._unlock()
+
+ def locked_get(self):
+ """Retrieve credential.
+
+ The Storage lock must be held when this is called.
+
+ Returns:
+ oauth2client.client.Credentials
+ """
+ credential = self._multistore._get_credential(self._key)
+ if credential:
+ credential.set_store(self)
+ return credential
+
+ def locked_put(self, credentials):
+ """Write a credential.
+
+ The Storage lock must be held when this is called.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ self._multistore._update_credential(self._key, credentials)
+
+ def locked_delete(self):
+ """Delete a credential.
+
+ The Storage lock must be held when this is called.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ self._multistore._delete_credential(self._key)
+
+ def _create_file_if_needed(self):
+ """Create an empty file if necessary.
+
+ This method will not initialize the file. Instead it implements a
+ simple version of "touch" to ensure the file has been created.
+ """
+ if not os.path.exists(self._file.filename()):
+ old_umask = os.umask(0o177)
+ try:
+ open(self._file.filename(), 'a+b').close()
+ finally:
+ os.umask(old_umask)
+
+ def _lock(self):
+ """Lock the entire multistore."""
+ self._thread_lock.acquire()
+ try:
+ self._file.open_and_lock()
+ except (IOError, OSError) as e:
+ if e.errno == errno.ENOSYS:
+ logger.warn('File system does not support locking the '
+ 'credentials file.')
+ elif e.errno == errno.ENOLCK:
+ logger.warn('File system is out of resources for writing the '
+ 'credentials file (is your disk full?).')
+ elif e.errno == errno.EDEADLK:
+ logger.warn('Lock contention on multistore file, opening '
+ 'in read-only mode.')
+ elif e.errno == errno.EACCES:
+ logger.warn('Cannot access credentials file.')
+ else:
+ raise
+ if not self._file.is_locked():
+ self._read_only = True
+ if self._warn_on_readonly:
+ logger.warn('The credentials file (%s) is not writable. '
+ 'Opening in read-only mode. Any refreshed '
+ 'credentials will only be '
+ 'valid for this run.', self._file.filename())
+
+ if os.path.getsize(self._file.filename()) == 0:
+ logger.debug('Initializing empty multistore file')
+ # The multistore is empty so write out an empty file.
+ self._data = {}
+ self._write()
+ elif not self._read_only or self._data is None:
+ # Only refresh the data if we are read/write or we haven't
+ # cached the data yet. If we are readonly, we assume is isn't
+ # changing out from under us and that we only have to read it
+ # once. This prevents us from whacking any new access keys that
+ # we have cached in memory but were unable to write out.
+ self._refresh_data_cache()
+
+ def _unlock(self):
+ """Release the lock on the multistore."""
+ self._file.unlock_and_close()
+ self._thread_lock.release()
+
+ def _locked_json_read(self):
+ """Get the raw content of the multistore file.
+
+ The multistore must be locked when this is called.
+
+ Returns:
+ The contents of the multistore decoded as JSON.
+ """
+ assert self._thread_lock.locked()
+ self._file.file_handle().seek(0)
+ return json.load(self._file.file_handle())
+
+ def _locked_json_write(self, data):
+ """Write a JSON serializable data structure to the multistore.
+
+ The multistore must be locked when this is called.
+
+ Args:
+ data: The data to be serialized and written.
+ """
+ assert self._thread_lock.locked()
+ if self._read_only:
+ return
+ self._file.file_handle().seek(0)
+ json.dump(data, self._file.file_handle(),
+ sort_keys=True, indent=2, separators=(',', ': '))
+ self._file.file_handle().truncate()
+
+ def _refresh_data_cache(self):
+ """Refresh the contents of the multistore.
+
+ The multistore must be locked when this is called.
+
+ Raises:
+ NewerCredentialStoreError: Raised when a newer client has written
+ the store.
+ """
+ self._data = {}
+ try:
+ raw_data = self._locked_json_read()
+ except Exception:
+ logger.warn('Credential data store could not be loaded. '
+ 'Will ignore and overwrite.')
+ return
+
+ version = 0
+ try:
+ version = raw_data['file_version']
+ except Exception:
+ logger.warn('Missing version for credential data store. It may be '
+ 'corrupt or an old version. Overwriting.')
+ if version > 1:
+ raise NewerCredentialStoreError(
+ 'Credential file has file_version of {0}. '
+ 'Only file_version of 1 is supported.'.format(version))
+
+ credentials = []
+ try:
+ credentials = raw_data['data']
+ except (TypeError, KeyError):
+ pass
+
+ for cred_entry in credentials:
+ try:
+ key, credential = self._decode_credential_from_json(cred_entry)
+ self._data[key] = credential
+ except:
+ # If something goes wrong loading a credential, just ignore it
+ logger.info('Error decoding credential, skipping',
+ exc_info=True)
+
+ def _decode_credential_from_json(self, cred_entry):
+ """Load a credential from our JSON serialization.
+
+ Args:
+ cred_entry: A dict entry from the data member of our format
+
+ Returns:
+ (key, cred) where the key is the key tuple and the cred is the
+ OAuth2Credential object.
+ """
+ raw_key = cred_entry['key']
+ key = _dict_to_tuple_key(raw_key)
+ credential = None
+ credential = client.Credentials.new_from_json(
+ json.dumps(cred_entry['credential']))
+ return (key, credential)
+
+ def _write(self):
+ """Write the cached data back out.
+
+ The multistore must be locked.
+ """
+ raw_data = {'file_version': 1}
+ raw_creds = []
+ raw_data['data'] = raw_creds
+ for (cred_key, cred) in self._data.items():
+ raw_key = dict(cred_key)
+ raw_cred = json.loads(cred.to_json())
+ raw_creds.append({'key': raw_key, 'credential': raw_cred})
+ self._locked_json_write(raw_data)
+
+ def _get_all_credential_keys(self):
+ """Gets all the registered credential keys in the multistore.
+
+ Returns:
+ A list of dictionaries corresponding to all the keys currently
+ registered
+ """
+ return [dict(key) for key in self._data.keys()]
+
+ def _get_credential(self, key):
+ """Get a credential from the multistore.
+
+ The multistore must be locked.
+
+ Args:
+ key: The key used to retrieve the credential
+
+ Returns:
+ The credential specified or None if not present
+ """
+ return self._data.get(key, None)
+
+ def _update_credential(self, key, cred):
+ """Update a credential and write the multistore.
+
+ This must be called when the multistore is locked.
+
+ Args:
+ key: The key used to retrieve the credential
+ cred: The OAuth2Credential to update/set
+ """
+ self._data[key] = cred
+ self._write()
+
+ def _delete_credential(self, key):
+ """Delete a credential and write the multistore.
+
+ This must be called when the multistore is locked.
+
+ Args:
+ key: The key used to retrieve the credential
+ """
+ try:
+ del self._data[key]
+ except KeyError:
+ pass
+ self._write()
+
+ def _get_storage(self, key):
+ """Get a Storage object to get/set a credential.
+
+ This Storage is a 'view' into the multistore.
+
+ Args:
+ key: The key used to retrieve the credential
+
+ Returns:
+ A Storage object that can be used to get/set this cred
+ """
+ return self._Storage(self, key)
diff --git a/oauth2client/contrib/sqlalchemy.py b/oauth2client/contrib/sqlalchemy.py
new file mode 100644
index 0000000..7d9fd4b
--- /dev/null
+++ b/oauth2client/contrib/sqlalchemy.py
@@ -0,0 +1,173 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""OAuth 2.0 utilities for SQLAlchemy.
+
+Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy.
+
+Configuration
+=============
+
+In order to use this storage, you'll need to create table
+with :class:`oauth2client.contrib.sqlalchemy.CredentialsType` column.
+It's recommended to either put this column on some sort of user info
+table or put the column in a table with a belongs-to relationship to
+a user info table.
+
+Here's an example of a simple table with a :class:`CredentialsType`
+column that's related to a user table by the `user_id` key.
+
+.. code-block:: python
+
+ from sqlalchemy import Column, ForeignKey, Integer
+ from sqlalchemy.ext.declarative import declarative_base
+ from sqlalchemy.orm import relationship
+
+ from oauth2client.contrib.sqlalchemy import CredentialsType
+
+
+ Base = declarative_base()
+
+
+ class Credentials(Base):
+ __tablename__ = 'credentials'
+
+ user_id = Column(Integer, ForeignKey('user.id'))
+ credentials = Column(CredentialsType)
+
+
+ class User(Base):
+ id = Column(Integer, primary_key=True)
+ # bunch of other columns
+ credentials = relationship('Credentials')
+
+
+Usage
+=====
+
+With tables ready, you are now able to store credentials in database.
+We will reuse tables defined above.
+
+.. code-block:: python
+
+ from sqlalchemy.orm import Session
+
+ from oauth2client.client import OAuth2Credentials
+ from oauth2client.contrib.sql_alchemy import Storage
+
+ session = Session()
+ user = session.query(User).first()
+ storage = Storage(
+ session=session,
+ model_class=Credentials,
+ # This is the key column used to identify
+ # the row that stores the credentials.
+ key_name='user_id',
+ key_value=user.id,
+ property_name='credentials',
+ )
+
+ # Store
+ credentials = OAuth2Credentials(...)
+ storage.put(credentials)
+
+ # Retrieve
+ credentials = storage.get()
+
+ # Delete
+ storage.delete()
+
+"""
+
+from __future__ import absolute_import
+
+import sqlalchemy.types
+
+from oauth2client import client
+
+
+class CredentialsType(sqlalchemy.types.PickleType):
+ """Type representing credentials.
+
+ Alias for :class:`sqlalchemy.types.PickleType`.
+ """
+
+
+class Storage(client.Storage):
+ """Store and retrieve a single credential to and from SQLAlchemy.
+ This helper presumes the Credentials
+ have been stored as a Credentials column
+ on a db model class.
+ """
+
+ def __init__(self, session, model_class, key_name,
+ key_value, property_name):
+ """Constructor for Storage.
+
+ Args:
+ session: An instance of :class:`sqlalchemy.orm.Session`.
+ model_class: SQLAlchemy declarative mapping.
+ key_name: string, key name for the entity that has the credentials
+ key_value: key value for the entity that has the credentials
+ property_name: A string indicating which property on the
+ ``model_class`` to store the credentials.
+ This property must be a
+ :class:`CredentialsType` column.
+ """
+ super(Storage, self).__init__()
+
+ self.session = session
+ self.model_class = model_class
+ self.key_name = key_name
+ self.key_value = key_value
+ self.property_name = property_name
+
+ def locked_get(self):
+ """Retrieve stored credential.
+
+ Returns:
+ A :class:`oauth2client.Credentials` instance or `None`.
+ """
+ filters = {self.key_name: self.key_value}
+ query = self.session.query(self.model_class).filter_by(**filters)
+ entity = query.first()
+
+ if entity:
+ credential = getattr(entity, self.property_name)
+ if credential and hasattr(credential, 'set_store'):
+ credential.set_store(self)
+ return credential
+ else:
+ return None
+
+ def locked_put(self, credentials):
+ """Write a credentials to the SQLAlchemy datastore.
+
+ Args:
+ credentials: :class:`oauth2client.Credentials`
+ """
+ filters = {self.key_name: self.key_value}
+ query = self.session.query(self.model_class).filter_by(**filters)
+ entity = query.first()
+
+ if not entity:
+ entity = self.model_class(**filters)
+
+ setattr(entity, self.property_name, credentials)
+ self.session.add(entity)
+
+ def locked_delete(self):
+ """Delete credentials from the SQLAlchemy datastore."""
+ filters = {self.key_name: self.key_value}
+ self.session.query(self.model_class).filter_by(**filters).delete()
diff --git a/oauth2client/contrib/xsrfutil.py b/oauth2client/contrib/xsrfutil.py
new file mode 100644
index 0000000..c03e679
--- /dev/null
+++ b/oauth2client/contrib/xsrfutil.py
@@ -0,0 +1,106 @@
+# Copyright 2014 the Melange authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper methods for creating & verifying XSRF tokens."""
+
+import base64
+import binascii
+import hmac
+import time
+
+from oauth2client import _helpers
+from oauth2client import util
+
+__authors__ = [
+ '"Doug Coker" <dcoker@google.com>',
+ '"Joe Gregorio" <jcgregorio@google.com>',
+]
+
+# Delimiter character
+DELIMITER = b':'
+
+# 1 hour in seconds
+DEFAULT_TIMEOUT_SECS = 60 * 60
+
+
+@util.positional(2)
+def generate_token(key, user_id, action_id='', when=None):
+ """Generates a URL-safe token for the given user, action, time tuple.
+
+ Args:
+ key: secret key to use.
+ user_id: the user ID of the authenticated user.
+ action_id: a string identifier of the action they requested
+ authorization for.
+ when: the time in seconds since the epoch at which the user was
+ authorized for this action. If not set the current time is used.
+
+ Returns:
+ A string XSRF protection token.
+ """
+ digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8'))
+ digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8'))
+ digester.update(DELIMITER)
+ digester.update(_helpers._to_bytes(action_id, encoding='utf-8'))
+ digester.update(DELIMITER)
+ when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8')
+ digester.update(when)
+ digest = digester.digest()
+
+ token = base64.urlsafe_b64encode(digest + DELIMITER + when)
+ return token
+
+
+@util.positional(3)
+def validate_token(key, token, user_id, action_id="", current_time=None):
+ """Validates that the given token authorizes the user for the action.
+
+ Tokens are invalid if the time of issue is too old or if the token
+ does not match what generateToken outputs (i.e. the token was forged).
+
+ Args:
+ key: secret key to use.
+ token: a string of the token generated by generateToken.
+ user_id: the user ID of the authenticated user.
+ action_id: a string identifier of the action they requested
+ authorization for.
+
+ Returns:
+ A boolean - True if the user is authorized for the action, False
+ otherwise.
+ """
+ if not token:
+ return False
+ try:
+ decoded = base64.urlsafe_b64decode(token)
+ token_time = int(decoded.split(DELIMITER)[-1])
+ except (TypeError, ValueError, binascii.Error):
+ return False
+ if current_time is None:
+ current_time = time.time()
+ # If the token is too old it's not valid.
+ if current_time - token_time > DEFAULT_TIMEOUT_SECS:
+ return False
+
+ # The given token should match the generated one with the same time.
+ expected_token = generate_token(key, user_id, action_id=action_id,
+ when=token_time)
+ if len(token) != len(expected_token):
+ return False
+
+ # Perform constant time comparison to avoid timing attacks
+ different = 0
+ for x, y in zip(bytearray(token), bytearray(expected_token)):
+ different |= x ^ y
+ return not different
diff --git a/oauth2client/crypt.py b/oauth2client/crypt.py
new file mode 100644
index 0000000..1326098
--- /dev/null
+++ b/oauth2client/crypt.py
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Crypto-related routines for oauth2client."""
+
+import json
+import logging
+import time
+
+from oauth2client import _helpers
+from oauth2client import _pure_python_crypt
+
+
+RsaSigner = _pure_python_crypt.RsaSigner
+RsaVerifier = _pure_python_crypt.RsaVerifier
+
+CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
+AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
+MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
+
+logger = logging.getLogger(__name__)
+
+
+class AppIdentityError(Exception):
+ """Error to indicate crypto failure."""
+
+
+def _bad_pkcs12_key_as_pem(*args, **kwargs):
+ raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
+
+
+try:
+ from oauth2client import _openssl_crypt
+ OpenSSLSigner = _openssl_crypt.OpenSSLSigner
+ OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
+ pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
+except ImportError: # pragma: NO COVER
+ OpenSSLVerifier = None
+ OpenSSLSigner = None
+ pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
+
+try:
+ from oauth2client import _pycrypto_crypt
+ PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
+ PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
+except ImportError: # pragma: NO COVER
+ PyCryptoVerifier = None
+ PyCryptoSigner = None
+
+
+if OpenSSLSigner:
+ Signer = OpenSSLSigner
+ Verifier = OpenSSLVerifier
+elif PyCryptoSigner: # pragma: NO COVER
+ Signer = PyCryptoSigner
+ Verifier = PyCryptoVerifier
+else: # pragma: NO COVER
+ Signer = RsaSigner
+ Verifier = RsaVerifier
+
+
+def make_signed_jwt(signer, payload, key_id=None):
+ """Make a signed JWT.
+
+ See http://self-issued.info/docs/draft-jones-json-web-token.html.
+
+ Args:
+ signer: crypt.Signer, Cryptographic signer.
+ payload: dict, Dictionary of data to convert to JSON and then sign.
+ key_id: string, (Optional) Key ID header.
+
+ Returns:
+ string, The JWT for the payload.
+ """
+ header = {'typ': 'JWT', 'alg': 'RS256'}
+ if key_id is not None:
+ header['kid'] = key_id
+
+ segments = [
+ _helpers._urlsafe_b64encode(_helpers._json_encode(header)),
+ _helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
+ ]
+ signing_input = b'.'.join(segments)
+
+ signature = signer.sign(signing_input)
+ segments.append(_helpers._urlsafe_b64encode(signature))
+
+ logger.debug(str(segments))
+
+ return b'.'.join(segments)
+
+
+def _verify_signature(message, signature, certs):
+ """Verifies signed content using a list of certificates.
+
+ Args:
+ message: string or bytes, The message to verify.
+ signature: string or bytes, The signature on the message.
+ certs: iterable, certificates in PEM format.
+
+ Raises:
+ AppIdentityError: If none of the certificates can verify the message
+ against the signature.
+ """
+ for pem in certs:
+ verifier = Verifier.from_string(pem, is_x509_cert=True)
+ if verifier.verify(message, signature):
+ return
+
+ # If we have not returned, no certificate confirms the signature.
+ raise AppIdentityError('Invalid token signature')
+
+
+def _check_audience(payload_dict, audience):
+ """Checks audience field from a JWT payload.
+
+ Does nothing if the passed in ``audience`` is null.
+
+ Args:
+ payload_dict: dict, A dictionary containing a JWT payload.
+ audience: string or NoneType, an audience to check for in
+ the JWT payload.
+
+ Raises:
+ AppIdentityError: If there is no ``'aud'`` field in the payload
+ dictionary but there is an ``audience`` to check.
+ AppIdentityError: If the ``'aud'`` field in the payload dictionary
+ does not match the ``audience``.
+ """
+ if audience is None:
+ return
+
+ audience_in_payload = payload_dict.get('aud')
+ if audience_in_payload is None:
+ raise AppIdentityError(
+ 'No aud field in token: {0}'.format(payload_dict))
+ if audience_in_payload != audience:
+ raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
+ audience_in_payload, audience, payload_dict))
+
+
+def _verify_time_range(payload_dict):
+ """Verifies the issued at and expiration from a JWT payload.
+
+ Makes sure the current time (in UTC) falls between the issued at and
+ expiration for the JWT (with some skew allowed for via
+ ``CLOCK_SKEW_SECS``).
+
+ Args:
+ payload_dict: dict, A dictionary containing a JWT payload.
+
+ Raises:
+ AppIdentityError: If there is no ``'iat'`` field in the payload
+ dictionary.
+ AppIdentityError: If there is no ``'exp'`` field in the payload
+ dictionary.
+ AppIdentityError: If the JWT expiration is too far in the future (i.e.
+ if the expiration would imply a token lifetime
+ longer than what is allowed.)
+ AppIdentityError: If the token appears to have been issued in the
+ future (up to clock skew).
+ AppIdentityError: If the token appears to have expired in the past
+ (up to clock skew).
+ """
+ # Get the current time to use throughout.
+ now = int(time.time())
+
+ # Make sure issued at and expiration are in the payload.
+ issued_at = payload_dict.get('iat')
+ if issued_at is None:
+ raise AppIdentityError(
+ 'No iat field in token: {0}'.format(payload_dict))
+ expiration = payload_dict.get('exp')
+ if expiration is None:
+ raise AppIdentityError(
+ 'No exp field in token: {0}'.format(payload_dict))
+
+ # Make sure the expiration gives an acceptable token lifetime.
+ if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
+ raise AppIdentityError(
+ 'exp field too far in future: {0}'.format(payload_dict))
+
+ # Make sure (up to clock skew) that the token wasn't issued in the future.
+ earliest = issued_at - CLOCK_SKEW_SECS
+ if now < earliest:
+ raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
+ now, earliest, payload_dict))
+ # Make sure (up to clock skew) that the token isn't already expired.
+ latest = expiration + CLOCK_SKEW_SECS
+ if now > latest:
+ raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
+ now, latest, payload_dict))
+
+
+def verify_signed_jwt_with_certs(jwt, certs, audience=None):
+ """Verify a JWT against public certs.
+
+ See http://self-issued.info/docs/draft-jones-json-web-token.html.
+
+ Args:
+ jwt: string, A JWT.
+ certs: dict, Dictionary where values of public keys in PEM format.
+ audience: string, The audience, 'aud', that this JWT should contain. If
+ None then the JWT's 'aud' parameter is not verified.
+
+ Returns:
+ dict, The deserialized JSON payload in the JWT.
+
+ Raises:
+ AppIdentityError: if any checks are failed.
+ """
+ jwt = _helpers._to_bytes(jwt)
+
+ if jwt.count(b'.') != 2:
+ raise AppIdentityError(
+ 'Wrong number of segments in token: {0}'.format(jwt))
+
+ header, payload, signature = jwt.split(b'.')
+ message_to_sign = header + b'.' + payload
+ signature = _helpers._urlsafe_b64decode(signature)
+
+ # Parse token.
+ payload_bytes = _helpers._urlsafe_b64decode(payload)
+ try:
+ payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
+ except:
+ raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
+
+ # Verify that the signature matches the message.
+ _verify_signature(message_to_sign, signature, certs.values())
+
+ # Verify the issued at and created times in the payload.
+ _verify_time_range(payload_dict)
+
+ # Check audience.
+ _check_audience(payload_dict, audience)
+
+ return payload_dict
diff --git a/oauth2client/file.py b/oauth2client/file.py
new file mode 100644
index 0000000..feede11
--- /dev/null
+++ b/oauth2client/file.py
@@ -0,0 +1,106 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for OAuth.
+
+Utilities for making it easier to work with OAuth 2.0
+credentials.
+"""
+
+import os
+import threading
+
+from oauth2client import client
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+class CredentialsFileSymbolicLinkError(Exception):
+ """Credentials files must not be symbolic links."""
+
+
+class Storage(client.Storage):
+ """Store and retrieve a single credential to and from a file."""
+
+ def __init__(self, filename):
+ super(Storage, self).__init__(lock=threading.Lock())
+ self._filename = filename
+
+ def _validate_file(self):
+ if os.path.islink(self._filename):
+ raise CredentialsFileSymbolicLinkError(
+ 'File: {0} is a symbolic link.'.format(self._filename))
+
+ def locked_get(self):
+ """Retrieve Credential from file.
+
+ Returns:
+ oauth2client.client.Credentials
+
+ Raises:
+ CredentialsFileSymbolicLinkError if the file is a symbolic link.
+ """
+ credentials = None
+ self._validate_file()
+ try:
+ f = open(self._filename, 'rb')
+ content = f.read()
+ f.close()
+ except IOError:
+ return credentials
+
+ try:
+ credentials = client.Credentials.new_from_json(content)
+ credentials.set_store(self)
+ except ValueError:
+ pass
+
+ return credentials
+
+ def _create_file_if_needed(self):
+ """Create an empty file if necessary.
+
+ This method will not initialize the file. Instead it implements a
+ simple version of "touch" to ensure the file has been created.
+ """
+ if not os.path.exists(self._filename):
+ old_umask = os.umask(0o177)
+ try:
+ open(self._filename, 'a+b').close()
+ finally:
+ os.umask(old_umask)
+
+ def locked_put(self, credentials):
+ """Write Credentials to file.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+
+ Raises:
+ CredentialsFileSymbolicLinkError if the file is a symbolic link.
+ """
+ self._create_file_if_needed()
+ self._validate_file()
+ f = open(self._filename, 'w')
+ f.write(credentials.to_json())
+ f.close()
+
+ def locked_delete(self):
+ """Delete Credentials file.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ os.unlink(self._filename)
diff --git a/oauth2client/service_account.py b/oauth2client/service_account.py
new file mode 100644
index 0000000..bdcfd69
--- /dev/null
+++ b/oauth2client/service_account.py
@@ -0,0 +1,673 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""oauth2client Service account credentials class."""
+
+import base64
+import copy
+import datetime
+import json
+import time
+
+import oauth2client
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client import crypt
+from oauth2client import transport
+from oauth2client import util
+
+
+_PASSWORD_DEFAULT = 'notasecret'
+_PKCS12_KEY = '_private_key_pkcs12'
+_PKCS12_ERROR = r"""
+This library only implements PKCS#12 support via the pyOpenSSL library.
+Either install pyOpenSSL, or please convert the .p12 file
+to .pem format:
+ $ cat key.p12 | \
+ > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
+ > openssl rsa > key.pem
+"""
+
+
+class ServiceAccountCredentials(client.AssertionCredentials):
+ """Service Account credential for OAuth 2.0 signed JWT grants.
+
+ Supports
+
+ * JSON keyfile (typically contains a PKCS8 key stored as
+ PEM text)
+ * ``.p12`` key (stores PKCS12 key and certificate)
+
+ Makes an assertion to server using a signed JWT assertion in exchange
+ for an access token.
+
+ This credential does not require a flow to instantiate because it
+ represents a two legged flow, and therefore has all of the required
+ information to generate and refresh its own access tokens.
+
+ Args:
+ service_account_email: string, The email associated with the
+ service account.
+ signer: ``crypt.Signer``, A signer which can be used to sign content.
+ scopes: List or string, (Optional) Scopes to use when acquiring
+ an access token.
+ private_key_id: string, (Optional) Private key identifier. Typically
+ only used with a JSON keyfile. Can be sent in the
+ header of a JWT token assertion.
+ client_id: string, (Optional) Client ID for the project that owns the
+ service account.
+ user_agent: string, (Optional) User agent to use when sending
+ request.
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ revoke_uri: string, URI for revoke endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ kwargs: dict, Extra key-value pairs (both strings) to send in the
+ payload body when making an assertion.
+ """
+
+ MAX_TOKEN_LIFETIME_SECS = 3600
+ """Max lifetime of the token (one hour, in seconds)."""
+
+ NON_SERIALIZED_MEMBERS = (
+ frozenset(['_signer']) |
+ client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
+ """Members that aren't serialized when object is converted to JSON."""
+
+ # Can be over-ridden by factory constructors. Used for
+ # serialization/deserialization purposes.
+ _private_key_pkcs8_pem = None
+ _private_key_pkcs12 = None
+ _private_key_password = None
+
+ def __init__(self,
+ service_account_email,
+ signer,
+ scopes='',
+ private_key_id=None,
+ client_id=None,
+ user_agent=None,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+ **kwargs):
+
+ super(ServiceAccountCredentials, self).__init__(
+ None, user_agent=user_agent, token_uri=token_uri,
+ revoke_uri=revoke_uri)
+
+ self._service_account_email = service_account_email
+ self._signer = signer
+ self._scopes = util.scopes_to_string(scopes)
+ self._private_key_id = private_key_id
+ self.client_id = client_id
+ self._user_agent = user_agent
+ self._kwargs = kwargs
+
+ def _to_json(self, strip, to_serialize=None):
+ """Utility function that creates JSON repr. of a credentials object.
+
+ Over-ride is needed since PKCS#12 keys will not in general be JSON
+ serializable.
+
+ Args:
+ strip: array, An array of names of members to exclude from the
+ JSON.
+ to_serialize: dict, (Optional) The properties for this object
+ that will be serialized. This allows callers to
+ modify before serializing.
+
+ Returns:
+ string, a JSON representation of this instance, suitable to pass to
+ from_json().
+ """
+ if to_serialize is None:
+ to_serialize = copy.copy(self.__dict__)
+ pkcs12_val = to_serialize.get(_PKCS12_KEY)
+ if pkcs12_val is not None:
+ to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
+ return super(ServiceAccountCredentials, self)._to_json(
+ strip, to_serialize=to_serialize)
+
+ @classmethod
+ def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
+ token_uri=None, revoke_uri=None):
+ """Helper for factory constructors from JSON keyfile.
+
+ Args:
+ keyfile_dict: dict-like object, The parsed dictionary-like object
+ containing the contents of the JSON keyfile.
+ scopes: List or string, Scopes to use when acquiring an
+ access token.
+ token_uri: string, URI for OAuth 2.0 provider token endpoint.
+ If unset and not present in keyfile_dict, defaults
+ to Google's endpoints.
+ revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
+ If unset and not present in keyfile_dict, defaults
+ to Google's endpoints.
+
+ Returns:
+ ServiceAccountCredentials, a credentials object created from
+ the keyfile contents.
+
+ Raises:
+ ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
+ KeyError, if one of the expected keys is not present in
+ the keyfile.
+ """
+ creds_type = keyfile_dict.get('type')
+ if creds_type != client.SERVICE_ACCOUNT:
+ raise ValueError('Unexpected credentials type', creds_type,
+ 'Expected', client.SERVICE_ACCOUNT)
+
+ service_account_email = keyfile_dict['client_email']
+ private_key_pkcs8_pem = keyfile_dict['private_key']
+ private_key_id = keyfile_dict['private_key_id']
+ client_id = keyfile_dict['client_id']
+ if not token_uri:
+ token_uri = keyfile_dict.get('token_uri',
+ oauth2client.GOOGLE_TOKEN_URI)
+ if not revoke_uri:
+ revoke_uri = keyfile_dict.get('revoke_uri',
+ oauth2client.GOOGLE_REVOKE_URI)
+
+ signer = crypt.Signer.from_string(private_key_pkcs8_pem)
+ credentials = cls(service_account_email, signer, scopes=scopes,
+ private_key_id=private_key_id,
+ client_id=client_id, token_uri=token_uri,
+ revoke_uri=revoke_uri)
+ credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
+ return credentials
+
+ @classmethod
+ def from_json_keyfile_name(cls, filename, scopes='',
+ token_uri=None, revoke_uri=None):
+
+ """Factory constructor from JSON keyfile by name.
+
+ Args:
+ filename: string, The location of the keyfile.
+ scopes: List or string, (Optional) Scopes to use when acquiring an
+ access token.
+ token_uri: string, URI for OAuth 2.0 provider token endpoint.
+ If unset and not present in the key file, defaults
+ to Google's endpoints.
+ revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
+ If unset and not present in the key file, defaults
+ to Google's endpoints.
+
+ Returns:
+ ServiceAccountCredentials, a credentials object created from
+ the keyfile.
+
+ Raises:
+ ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
+ KeyError, if one of the expected keys is not present in
+ the keyfile.
+ """
+ with open(filename, 'r') as file_obj:
+ client_credentials = json.load(file_obj)
+ return cls._from_parsed_json_keyfile(client_credentials, scopes,
+ token_uri=token_uri,
+ revoke_uri=revoke_uri)
+
+ @classmethod
+ def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
+ token_uri=None, revoke_uri=None):
+ """Factory constructor from parsed JSON keyfile.
+
+ Args:
+ keyfile_dict: dict-like object, The parsed dictionary-like object
+ containing the contents of the JSON keyfile.
+ scopes: List or string, (Optional) Scopes to use when acquiring an
+ access token.
+ token_uri: string, URI for OAuth 2.0 provider token endpoint.
+ If unset and not present in keyfile_dict, defaults
+ to Google's endpoints.
+ revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
+ If unset and not present in keyfile_dict, defaults
+ to Google's endpoints.
+
+ Returns:
+ ServiceAccountCredentials, a credentials object created from
+ the keyfile.
+
+ Raises:
+ ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
+ KeyError, if one of the expected keys is not present in
+ the keyfile.
+ """
+ return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
+ token_uri=token_uri,
+ revoke_uri=revoke_uri)
+
+ @classmethod
+ def _from_p12_keyfile_contents(cls, service_account_email,
+ private_key_pkcs12,
+ private_key_password=None, scopes='',
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+ """Factory constructor from JSON keyfile.
+
+ Args:
+ service_account_email: string, The email associated with the
+ service account.
+ private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
+ private_key_password: string, (Optional) Password for PKCS#12
+ private key. Defaults to ``notasecret``.
+ scopes: List or string, (Optional) Scopes to use when acquiring an
+ access token.
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ revoke_uri: string, URI for revoke endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0
+ provider can be used.
+
+ Returns:
+ ServiceAccountCredentials, a credentials object created from
+ the keyfile.
+
+ Raises:
+ NotImplementedError if pyOpenSSL is not installed / not the
+ active crypto library.
+ """
+ if private_key_password is None:
+ private_key_password = _PASSWORD_DEFAULT
+ if crypt.Signer is not crypt.OpenSSLSigner:
+ raise NotImplementedError(_PKCS12_ERROR)
+ signer = crypt.Signer.from_string(private_key_pkcs12,
+ private_key_password)
+ credentials = cls(service_account_email, signer, scopes=scopes,
+ token_uri=token_uri, revoke_uri=revoke_uri)
+ credentials._private_key_pkcs12 = private_key_pkcs12
+ credentials._private_key_password = private_key_password
+ return credentials
+
+ @classmethod
+ def from_p12_keyfile(cls, service_account_email, filename,
+ private_key_password=None, scopes='',
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+
+ """Factory constructor from JSON keyfile.
+
+ Args:
+ service_account_email: string, The email associated with the
+ service account.
+ filename: string, The location of the PKCS#12 keyfile.
+ private_key_password: string, (Optional) Password for PKCS#12
+ private key. Defaults to ``notasecret``.
+ scopes: List or string, (Optional) Scopes to use when acquiring an
+ access token.
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ revoke_uri: string, URI for revoke endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0
+ provider can be used.
+
+ Returns:
+ ServiceAccountCredentials, a credentials object created from
+ the keyfile.
+
+ Raises:
+ NotImplementedError if pyOpenSSL is not installed / not the
+ active crypto library.
+ """
+ with open(filename, 'rb') as file_obj:
+ private_key_pkcs12 = file_obj.read()
+ return cls._from_p12_keyfile_contents(
+ service_account_email, private_key_pkcs12,
+ private_key_password=private_key_password, scopes=scopes,
+ token_uri=token_uri, revoke_uri=revoke_uri)
+
+ @classmethod
+ def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
+ private_key_password=None, scopes='',
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+ """Factory constructor from JSON keyfile.
+
+ Args:
+ service_account_email: string, The email associated with the
+ service account.
+ file_buffer: stream, A buffer that implements ``read()``
+ and contains the PKCS#12 key contents.
+ private_key_password: string, (Optional) Password for PKCS#12
+ private key. Defaults to ``notasecret``.
+ scopes: List or string, (Optional) Scopes to use when acquiring an
+ access token.
+ token_uri: string, URI for token endpoint. For convenience defaults
+ to Google's endpoints but any OAuth 2.0 provider can be
+ used.
+ revoke_uri: string, URI for revoke endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0
+ provider can be used.
+
+ Returns:
+ ServiceAccountCredentials, a credentials object created from
+ the keyfile.
+
+ Raises:
+ NotImplementedError if pyOpenSSL is not installed / not the
+ active crypto library.
+ """
+ private_key_pkcs12 = file_buffer.read()
+ return cls._from_p12_keyfile_contents(
+ service_account_email, private_key_pkcs12,
+ private_key_password=private_key_password, scopes=scopes,
+ token_uri=token_uri, revoke_uri=revoke_uri)
+
+ def _generate_assertion(self):
+ """Generate the assertion that will be used in the request."""
+ now = int(time.time())
+ payload = {
+ 'aud': self.token_uri,
+ 'scope': self._scopes,
+ 'iat': now,
+ 'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
+ 'iss': self._service_account_email,
+ }
+ payload.update(self._kwargs)
+ return crypt.make_signed_jwt(self._signer, payload,
+ key_id=self._private_key_id)
+
+ def sign_blob(self, blob):
+ """Cryptographically sign a blob (of bytes).
+
+ Implements abstract method
+ :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
+
+ Args:
+ blob: bytes, Message to be signed.
+
+ Returns:
+ tuple, A pair of the private key ID used to sign the blob and
+ the signed contents.
+ """
+ return self._private_key_id, self._signer.sign(blob)
+
+ @property
+ def service_account_email(self):
+ """Get the email for the current service account.
+
+ Returns:
+ string, The email associated with the service account.
+ """
+ return self._service_account_email
+
+ @property
+ def serialization_data(self):
+ # NOTE: This is only useful for JSON keyfile.
+ return {
+ 'type': 'service_account',
+ 'client_email': self._service_account_email,
+ 'private_key_id': self._private_key_id,
+ 'private_key': self._private_key_pkcs8_pem,
+ 'client_id': self.client_id,
+ }
+
+ @classmethod
+ def from_json(cls, json_data):
+ """Deserialize a JSON-serialized instance.
+
+ Inverse to :meth:`to_json`.
+
+ Args:
+ json_data: dict or string, Serialized JSON (as a string or an
+ already parsed dictionary) representing a credential.
+
+ Returns:
+ ServiceAccountCredentials from the serialized data.
+ """
+ if not isinstance(json_data, dict):
+ json_data = json.loads(_helpers._from_bytes(json_data))
+
+ private_key_pkcs8_pem = None
+ pkcs12_val = json_data.get(_PKCS12_KEY)
+ password = None
+ if pkcs12_val is None:
+ private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
+ signer = crypt.Signer.from_string(private_key_pkcs8_pem)
+ else:
+ # NOTE: This assumes that private_key_pkcs8_pem is not also
+ # in the serialized data. This would be very incorrect
+ # state.
+ pkcs12_val = base64.b64decode(pkcs12_val)
+ password = json_data['_private_key_password']
+ signer = crypt.Signer.from_string(pkcs12_val, password)
+
+ credentials = cls(
+ json_data['_service_account_email'],
+ signer,
+ scopes=json_data['_scopes'],
+ private_key_id=json_data['_private_key_id'],
+ client_id=json_data['client_id'],
+ user_agent=json_data['_user_agent'],
+ **json_data['_kwargs']
+ )
+ if private_key_pkcs8_pem is not None:
+ credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
+ if pkcs12_val is not None:
+ credentials._private_key_pkcs12 = pkcs12_val
+ if password is not None:
+ credentials._private_key_password = password
+ credentials.invalid = json_data['invalid']
+ credentials.access_token = json_data['access_token']
+ credentials.token_uri = json_data['token_uri']
+ credentials.revoke_uri = json_data['revoke_uri']
+ token_expiry = json_data.get('token_expiry', None)
+ if token_expiry is not None:
+ credentials.token_expiry = datetime.datetime.strptime(
+ token_expiry, client.EXPIRY_FORMAT)
+ return credentials
+
+ def create_scoped_required(self):
+ return not self._scopes
+
+ def create_scoped(self, scopes):
+ result = self.__class__(self._service_account_email,
+ self._signer,
+ scopes=scopes,
+ private_key_id=self._private_key_id,
+ client_id=self.client_id,
+ user_agent=self._user_agent,
+ **self._kwargs)
+ result.token_uri = self.token_uri
+ result.revoke_uri = self.revoke_uri
+ result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
+ result._private_key_pkcs12 = self._private_key_pkcs12
+ result._private_key_password = self._private_key_password
+ return result
+
+ def create_with_claims(self, claims):
+ """Create credentials that specify additional claims.
+
+ Args:
+ claims: dict, key-value pairs for claims.
+
+ Returns:
+ ServiceAccountCredentials, a copy of the current service account
+ credentials with updated claims to use when obtaining access
+ tokens.
+ """
+ new_kwargs = dict(self._kwargs)
+ new_kwargs.update(claims)
+ result = self.__class__(self._service_account_email,
+ self._signer,
+ scopes=self._scopes,
+ private_key_id=self._private_key_id,
+ client_id=self.client_id,
+ user_agent=self._user_agent,
+ **new_kwargs)
+ result.token_uri = self.token_uri
+ result.revoke_uri = self.revoke_uri
+ result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
+ result._private_key_pkcs12 = self._private_key_pkcs12
+ result._private_key_password = self._private_key_password
+ return result
+
+ def create_delegated(self, sub):
+ """Create credentials that act as domain-wide delegation of authority.
+
+ Use the ``sub`` parameter as the subject to delegate on behalf of
+ that user.
+
+ For example::
+
+ >>> account_sub = 'foo@email.com'
+ >>> delegate_creds = creds.create_delegated(account_sub)
+
+ Args:
+ sub: string, An email address that this service account will
+ act on behalf of (via domain-wide delegation).
+
+ Returns:
+ ServiceAccountCredentials, a copy of the current service account
+ updated to act on behalf of ``sub``.
+ """
+ return self.create_with_claims({'sub': sub})
+
+
+def _datetime_to_secs(utc_time):
+ # TODO(issue 298): use time_delta.total_seconds()
+ # time_delta.total_seconds() not supported in Python 2.6
+ epoch = datetime.datetime(1970, 1, 1)
+ time_delta = utc_time - epoch
+ return time_delta.days * 86400 + time_delta.seconds
+
+
+class _JWTAccessCredentials(ServiceAccountCredentials):
+ """Self signed JWT credentials.
+
+ Makes an assertion to server using a self signed JWT from service account
+ credentials. These credentials do NOT use OAuth 2.0 and instead
+ authenticate directly.
+ """
+ _MAX_TOKEN_LIFETIME_SECS = 3600
+ """Max lifetime of the token (one hour, in seconds)."""
+
+ def __init__(self,
+ service_account_email,
+ signer,
+ scopes=None,
+ private_key_id=None,
+ client_id=None,
+ user_agent=None,
+ token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
+ additional_claims=None):
+ if additional_claims is None:
+ additional_claims = {}
+ super(_JWTAccessCredentials, self).__init__(
+ service_account_email,
+ signer,
+ private_key_id=private_key_id,
+ client_id=client_id,
+ user_agent=user_agent,
+ token_uri=token_uri,
+ revoke_uri=revoke_uri,
+ **additional_claims)
+
+ def authorize(self, http):
+ """Authorize an httplib2.Http instance with a JWT assertion.
+
+ Unless specified, the 'aud' of the assertion will be the base
+ uri of the request.
+
+ Args:
+ http: An instance of ``httplib2.Http`` or something that acts
+ like it.
+ Returns:
+ A modified instance of http that was passed in.
+ Example::
+ h = httplib2.Http()
+ h = credentials.authorize(h)
+ """
+ transport.wrap_http_for_jwt_access(self, http)
+ return http
+
+ def get_access_token(self, http=None, additional_claims=None):
+ """Create a signed jwt.
+
+ Args:
+ http: unused
+ additional_claims: dict, additional claims to add to
+ the payload of the JWT.
+ Returns:
+ An AccessTokenInfo with the signed jwt
+ """
+ if additional_claims is None:
+ if self.access_token is None or self.access_token_expired:
+ self.refresh(None)
+ return client.AccessTokenInfo(
+ access_token=self.access_token, expires_in=self._expires_in())
+ else:
+ # Create a 1 time token
+ token, unused_expiry = self._create_token(additional_claims)
+ return client.AccessTokenInfo(
+ access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
+
+ def revoke(self, http):
+ """Cannot revoke JWTAccessCredentials tokens."""
+ pass
+
+ def create_scoped_required(self):
+ # JWTAccessCredentials are unscoped by definition
+ return True
+
+ def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
+ revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
+ # Returns an OAuth2 credentials with the given scope
+ result = ServiceAccountCredentials(self._service_account_email,
+ self._signer,
+ scopes=scopes,
+ private_key_id=self._private_key_id,
+ client_id=self.client_id,
+ user_agent=self._user_agent,
+ token_uri=token_uri,
+ revoke_uri=revoke_uri,
+ **self._kwargs)
+ if self._private_key_pkcs8_pem is not None:
+ result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
+ if self._private_key_pkcs12 is not None:
+ result._private_key_pkcs12 = self._private_key_pkcs12
+ if self._private_key_password is not None:
+ result._private_key_password = self._private_key_password
+ return result
+
+ def refresh(self, http):
+ self._refresh(None)
+
+ def _refresh(self, http_request):
+ self.access_token, self.token_expiry = self._create_token()
+
+ def _create_token(self, additional_claims=None):
+ now = client._UTCNOW()
+ lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS)
+ expiry = now + lifetime
+ payload = {
+ 'iat': _datetime_to_secs(now),
+ 'exp': _datetime_to_secs(expiry),
+ 'iss': self._service_account_email,
+ 'sub': self._service_account_email
+ }
+ payload.update(self._kwargs)
+ if additional_claims is not None:
+ payload.update(additional_claims)
+ jwt = crypt.make_signed_jwt(self._signer, payload,
+ key_id=self._private_key_id)
+ return jwt.decode('ascii'), expiry
diff --git a/oauth2client/tools.py b/oauth2client/tools.py
new file mode 100644
index 0000000..8947157
--- /dev/null
+++ b/oauth2client/tools.py
@@ -0,0 +1,256 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Command-line tools for authenticating via OAuth 2.0
+
+Do the OAuth 2.0 Web Server dance for a command line application. Stores the
+generated credentials in a common file that is used by other example apps in
+the same directory.
+"""
+
+from __future__ import print_function
+
+import logging
+import socket
+import sys
+
+from six.moves import BaseHTTPServer
+from six.moves import http_client
+from six.moves import input
+from six.moves import urllib
+
+from oauth2client import client
+from oauth2client import util
+
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+__all__ = ['argparser', 'run_flow', 'message_if_missing']
+
+_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
+
+To make this sample run you will need to populate the client_secrets.json file
+found at:
+
+ {file_path}
+
+with information from the APIs Console <https://code.google.com/apis/console>.
+
+"""
+
+_FAILED_START_MESSAGE = """
+Failed to start a local webserver listening on either port 8080
+or port 8090. Please check your firewall settings and locally
+running programs that may be blocking or using those ports.
+
+Falling back to --noauth_local_webserver and continuing with
+authorization.
+"""
+
+_BROWSER_OPENED_MESSAGE = """
+Your browser has been opened to visit:
+
+ {address}
+
+If your browser is on a different machine then exit and re-run this
+application with the command-line parameter
+
+ --noauth_local_webserver
+"""
+
+_GO_TO_LINK_MESSAGE = """
+Go to the following link in your browser:
+
+ {address}
+"""
+
+
+def _CreateArgumentParser():
+ try:
+ import argparse
+ except ImportError: # pragma: NO COVER
+ return None
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument('--auth_host_name', default='localhost',
+ help='Hostname when running a local web server.')
+ parser.add_argument('--noauth_local_webserver', action='store_true',
+ default=False, help='Do not run a local web server.')
+ parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
+ nargs='*', help='Port web server should listen on.')
+ parser.add_argument(
+ '--logging_level', default='ERROR',
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
+ help='Set the logging level of detail.')
+ return parser
+
+# argparser is an ArgumentParser that contains command-line options expected
+# by tools.run(). Pass it in as part of the 'parents' argument to your own
+# ArgumentParser.
+argparser = _CreateArgumentParser()
+
+
+class ClientRedirectServer(BaseHTTPServer.HTTPServer):
+ """A server to handle OAuth 2.0 redirects back to localhost.
+
+ Waits for a single request and parses the query parameters
+ into query_params and then stops serving.
+ """
+ query_params = {}
+
+
+class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """A handler for OAuth 2.0 redirects back to localhost.
+
+ Waits for a single request and parses the query parameters
+ into the servers query_params and then stops serving.
+ """
+
+ def do_GET(self):
+ """Handle a GET request.
+
+ Parses the query parameters and prints a message
+ if the flow has completed. Note that we can't detect
+ if an error occurred.
+ """
+ self.send_response(http_client.OK)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+ query = self.path.split('?', 1)[-1]
+ query = dict(urllib.parse.parse_qsl(query))
+ self.server.query_params = query
+ self.wfile.write(
+ b"<html><head><title>Authentication Status</title></head>")
+ self.wfile.write(
+ b"<body><p>The authentication flow has completed.</p>")
+ self.wfile.write(b"</body></html>")
+
+ def log_message(self, format, *args):
+ """Do not log messages to stdout while running as cmd. line program."""
+
+
+@util.positional(3)
+def run_flow(flow, storage, flags=None, http=None):
+ """Core code for a command-line application.
+
+ The ``run()`` function is called from your application and runs
+ through all the steps to obtain credentials. It takes a ``Flow``
+ argument and attempts to open an authorization server page in the
+ user's default web browser. The server asks the user to grant your
+ application access to the user's data. If the user grants access,
+ the ``run()`` function returns new credentials. The new credentials
+ are also stored in the ``storage`` argument, which updates the file
+ associated with the ``Storage`` object.
+
+ It presumes it is run from a command-line application and supports the
+ following flags:
+
+ ``--auth_host_name`` (string, default: ``localhost``)
+ Host name to use when running a local web server to handle
+ redirects during OAuth authorization.
+
+ ``--auth_host_port`` (integer, default: ``[8080, 8090]``)
+ Port to use when running a local web server to handle redirects
+ during OAuth authorization. Repeat this option to specify a list
+ of values.
+
+ ``--[no]auth_local_webserver`` (boolean, default: ``True``)
+ Run a local web server to handle redirects during OAuth
+ authorization.
+
+ The tools module defines an ``ArgumentParser`` the already contains the
+ flag definitions that ``run()`` requires. You can pass that
+ ``ArgumentParser`` to your ``ArgumentParser`` constructor::
+
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ parents=[tools.argparser])
+ flags = parser.parse_args(argv)
+
+ Args:
+ flow: Flow, an OAuth 2.0 Flow to step through.
+ storage: Storage, a ``Storage`` to store the credential in.
+ flags: ``argparse.Namespace``, (Optional) The command-line flags. This
+ is the object returned from calling ``parse_args()`` on
+ ``argparse.ArgumentParser`` as described above. Defaults
+ to ``argparser.parse_args()``.
+ http: An instance of ``httplib2.Http.request`` or something that
+ acts like it.
+
+ Returns:
+ Credentials, the obtained credential.
+ """
+ if flags is None:
+ flags = argparser.parse_args()
+ logging.getLogger().setLevel(getattr(logging, flags.logging_level))
+ if not flags.noauth_local_webserver:
+ success = False
+ port_number = 0
+ for port in flags.auth_host_port:
+ port_number = port
+ try:
+ httpd = ClientRedirectServer((flags.auth_host_name, port),
+ ClientRedirectHandler)
+ except socket.error:
+ pass
+ else:
+ success = True
+ break
+ flags.noauth_local_webserver = not success
+ if not success:
+ print(_FAILED_START_MESSAGE)
+
+ if not flags.noauth_local_webserver:
+ oauth_callback = 'http://{host}:{port}/'.format(
+ host=flags.auth_host_name, port=port_number)
+ else:
+ oauth_callback = client.OOB_CALLBACK_URN
+ flow.redirect_uri = oauth_callback
+ authorize_url = flow.step1_get_authorize_url()
+
+ if not flags.noauth_local_webserver:
+ import webbrowser
+ webbrowser.open(authorize_url, new=1, autoraise=True)
+ print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url))
+ else:
+ print(_GO_TO_LINK_MESSAGE.format(address=authorize_url))
+
+ code = None
+ if not flags.noauth_local_webserver:
+ httpd.handle_request()
+ if 'error' in httpd.query_params:
+ sys.exit('Authentication request was rejected.')
+ if 'code' in httpd.query_params:
+ code = httpd.query_params['code']
+ else:
+ print('Failed to find "code" in the query parameters '
+ 'of the redirect.')
+ sys.exit('Try running with --noauth_local_webserver.')
+ else:
+ code = input('Enter verification code: ').strip()
+
+ try:
+ credential = flow.step2_exchange(code, http=http)
+ except client.FlowExchangeError as e:
+ sys.exit('Authentication has failed: {0}'.format(e))
+
+ storage.put(credential)
+ credential.set_store(storage)
+ print('Authentication successful.')
+
+ return credential
+
+
+def message_if_missing(filename):
+ """Helpful message to display if the CLIENT_SECRETS file is missing."""
+ return _CLIENT_SECRETS_MESSAGE.format(file_path=filename)
diff --git a/oauth2client/transport.py b/oauth2client/transport.py
new file mode 100644
index 0000000..8dbc60d
--- /dev/null
+++ b/oauth2client/transport.py
@@ -0,0 +1,245 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+import httplib2
+import six
+from six.moves import http_client
+
+from oauth2client._helpers import _to_bytes
+
+
+_LOGGER = logging.getLogger(__name__)
+# Properties present in file-like streams / buffers.
+_STREAM_PROPERTIES = ('read', 'seek', 'tell')
+
+# Google Data client libraries may need to set this to [401, 403].
+REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
+
+
+class MemoryCache(object):
+ """httplib2 Cache implementation which only caches locally."""
+
+ def __init__(self):
+ self.cache = {}
+
+ def get(self, key):
+ return self.cache.get(key)
+
+ def set(self, key, value):
+ self.cache[key] = value
+
+ def delete(self, key):
+ self.cache.pop(key, None)
+
+
+def get_cached_http():
+ """Return an HTTP object which caches results returned.
+
+ This is intended to be used in methods like
+ oauth2client.client.verify_id_token(), which calls to the same URI
+ to retrieve certs.
+
+ Returns:
+ httplib2.Http, an HTTP object with a MemoryCache
+ """
+ return _CACHED_HTTP
+
+
+def get_http_object():
+ """Return a new HTTP object.
+
+ Returns:
+ httplib2.Http, an HTTP object.
+ """
+ return httplib2.Http()
+
+
+def _initialize_headers(headers):
+ """Creates a copy of the headers.
+
+ Args:
+ headers: dict, request headers to copy.
+
+ Returns:
+ dict, the copied headers or a new dictionary if the headers
+ were None.
+ """
+ return {} if headers is None else dict(headers)
+
+
+def _apply_user_agent(headers, user_agent):
+ """Adds a user-agent to the headers.
+
+ Args:
+ headers: dict, request headers to add / modify user
+ agent within.
+ user_agent: str, the user agent to add.
+
+ Returns:
+ dict, the original headers passed in, but modified if the
+ user agent is not None.
+ """
+ if user_agent is not None:
+ if 'user-agent' in headers:
+ headers['user-agent'] = (user_agent + ' ' + headers['user-agent'])
+ else:
+ headers['user-agent'] = user_agent
+
+ return headers
+
+
+def clean_headers(headers):
+ """Forces header keys and values to be strings, i.e not unicode.
+
+ The httplib module just concats the header keys and values in a way that
+ may make the message header a unicode string, which, if it then tries to
+ contatenate to a binary request body may result in a unicode decode error.
+
+ Args:
+ headers: dict, A dictionary of headers.
+
+ Returns:
+ The same dictionary but with all the keys converted to strings.
+ """
+ clean = {}
+ try:
+ for k, v in six.iteritems(headers):
+ if not isinstance(k, six.binary_type):
+ k = str(k)
+ if not isinstance(v, six.binary_type):
+ v = str(v)
+ clean[_to_bytes(k)] = _to_bytes(v)
+ except UnicodeEncodeError:
+ from oauth2client.client import NonAsciiHeaderError
+ raise NonAsciiHeaderError(k, ': ', v)
+ return clean
+
+
+def wrap_http_for_auth(credentials, http):
+ """Prepares an HTTP object's request method for auth.
+
+ Wraps HTTP requests with logic to catch auth failures (typically
+ identified via a 401 status code). In the event of failure, tries
+ to refresh the token used and then retry the original request.
+
+ Args:
+ credentials: Credentials, the credentials used to identify
+ the authenticated user.
+ http: httplib2.Http, an http object to be used to make
+ auth requests.
+ """
+ orig_request_method = http.request
+
+ # The closure that will replace 'httplib2.Http.request'.
+ def new_request(uri, method='GET', body=None, headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+ connection_type=None):
+ if not credentials.access_token:
+ _LOGGER.info('Attempting refresh to obtain '
+ 'initial access_token')
+ credentials._refresh(orig_request_method)
+
+ # Clone and modify the request headers to add the appropriate
+ # Authorization header.
+ headers = _initialize_headers(headers)
+ credentials.apply(headers)
+ _apply_user_agent(headers, credentials.user_agent)
+
+ body_stream_position = None
+ # Check if the body is a file-like stream.
+ if all(getattr(body, stream_prop, None) for stream_prop in
+ _STREAM_PROPERTIES):
+ body_stream_position = body.tell()
+
+ resp, content = orig_request_method(uri, method, body,
+ clean_headers(headers),
+ redirections, connection_type)
+
+ # A stored token may expire between the time it is retrieved and
+ # the time the request is made, so we may need to try twice.
+ max_refresh_attempts = 2
+ for refresh_attempt in range(max_refresh_attempts):
+ if resp.status not in REFRESH_STATUS_CODES:
+ break
+ _LOGGER.info('Refreshing due to a %s (attempt %s/%s)',
+ resp.status, refresh_attempt + 1,
+ max_refresh_attempts)
+ credentials._refresh(orig_request_method)
+ credentials.apply(headers)
+ if body_stream_position is not None:
+ body.seek(body_stream_position)
+
+ resp, content = orig_request_method(uri, method, body,
+ clean_headers(headers),
+ redirections, connection_type)
+
+ return resp, content
+
+ # Replace the request method with our own closure.
+ http.request = new_request
+
+ # Set credentials as a property of the request method.
+ setattr(http.request, 'credentials', credentials)
+
+
+def wrap_http_for_jwt_access(credentials, http):
+ """Prepares an HTTP object's request method for JWT access.
+
+ Wraps HTTP requests with logic to catch auth failures (typically
+ identified via a 401 status code). In the event of failure, tries
+ to refresh the token used and then retry the original request.
+
+ Args:
+ credentials: _JWTAccessCredentials, the credentials used to identify
+ a service account that uses JWT access tokens.
+ http: httplib2.Http, an http object to be used to make
+ auth requests.
+ """
+ orig_request_method = http.request
+ wrap_http_for_auth(credentials, http)
+ # The new value of ``http.request`` set by ``wrap_http_for_auth``.
+ authenticated_request_method = http.request
+
+ # The closure that will replace 'httplib2.Http.request'.
+ def new_request(uri, method='GET', body=None, headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+ connection_type=None):
+ if 'aud' in credentials._kwargs:
+ # Preemptively refresh token, this is not done for OAuth2
+ if (credentials.access_token is None or
+ credentials.access_token_expired):
+ credentials.refresh(None)
+ return authenticated_request_method(uri, method, body,
+ headers, redirections,
+ connection_type)
+ else:
+ # If we don't have an 'aud' (audience) claim,
+ # create a 1-time token with the uri root as the audience
+ headers = _initialize_headers(headers)
+ _apply_user_agent(headers, credentials.user_agent)
+ uri_root = uri.split('?', 1)[0]
+ token, unused_expiry = credentials._create_token({'aud': uri_root})
+
+ headers['Authorization'] = 'Bearer ' + token
+ return orig_request_method(uri, method, body,
+ clean_headers(headers),
+ redirections, connection_type)
+
+ # Replace the request method with our own closure.
+ http.request = new_request
+
+
+_CACHED_HTTP = httplib2.Http(MemoryCache())
diff --git a/oauth2client/util.py b/oauth2client/util.py
new file mode 100644
index 0000000..e3ba62b
--- /dev/null
+++ b/oauth2client/util.py
@@ -0,0 +1,206 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Common utility library."""
+
+import functools
+import inspect
+import logging
+
+import six
+from six.moves import urllib
+
+
+__author__ = [
+ 'rafek@google.com (Rafe Kaplan)',
+ 'guido@google.com (Guido van Rossum)',
+]
+
+__all__ = [
+ 'positional',
+ 'POSITIONAL_WARNING',
+ 'POSITIONAL_EXCEPTION',
+ 'POSITIONAL_IGNORE',
+]
+
+logger = logging.getLogger(__name__)
+
+POSITIONAL_WARNING = 'WARNING'
+POSITIONAL_EXCEPTION = 'EXCEPTION'
+POSITIONAL_IGNORE = 'IGNORE'
+POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
+ POSITIONAL_IGNORE])
+
+positional_parameters_enforcement = POSITIONAL_WARNING
+
+
+def positional(max_positional_args):
+ """A decorator to declare that only the first N arguments my be positional.
+
+ This decorator makes it easy to support Python 3 style keyword-only
+ parameters. For example, in Python 3 it is possible to write::
+
+ def fn(pos1, *, kwonly1=None, kwonly1=None):
+ ...
+
+ All named parameters after ``*`` must be a keyword::
+
+ fn(10, 'kw1', 'kw2') # Raises exception.
+ fn(10, kwonly1='kw1') # Ok.
+
+ Example
+ ^^^^^^^
+
+ To define a function like above, do::
+
+ @positional(1)
+ def fn(pos1, kwonly1=None, kwonly2=None):
+ ...
+
+ If no default value is provided to a keyword argument, it becomes a
+ required keyword argument::
+
+ @positional(0)
+ def fn(required_kw):
+ ...
+
+ This must be called with the keyword parameter::
+
+ fn() # Raises exception.
+ fn(10) # Raises exception.
+ fn(required_kw=10) # Ok.
+
+ When defining instance or class methods always remember to account for
+ ``self`` and ``cls``::
+
+ class MyClass(object):
+
+ @positional(2)
+ def my_method(self, pos1, kwonly1=None):
+ ...
+
+ @classmethod
+ @positional(2)
+ def my_method(cls, pos1, kwonly1=None):
+ ...
+
+ The positional decorator behavior is controlled by
+ ``util.positional_parameters_enforcement``, which may be set to
+ ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
+ ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
+ nothing, respectively, if a declaration is violated.
+
+ Args:
+ max_positional_arguments: Maximum number of positional arguments. All
+ parameters after the this index must be
+ keyword only.
+
+ Returns:
+ A decorator that prevents using arguments after max_positional_args
+ from being used as positional parameters.
+
+ Raises:
+ TypeError: if a key-word only argument is provided as a positional
+ parameter, but only if
+ util.positional_parameters_enforcement is set to
+ POSITIONAL_EXCEPTION.
+ """
+
+ def positional_decorator(wrapped):
+ @functools.wraps(wrapped)
+ def positional_wrapper(*args, **kwargs):
+ if len(args) > max_positional_args:
+ plural_s = ''
+ if max_positional_args != 1:
+ plural_s = 's'
+ message = ('{function}() takes at most {args_max} positional '
+ 'argument{plural} ({args_given} given)'.format(
+ function=wrapped.__name__,
+ args_max=max_positional_args,
+ args_given=len(args),
+ plural=plural_s))
+ if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
+ raise TypeError(message)
+ elif positional_parameters_enforcement == POSITIONAL_WARNING:
+ logger.warning(message)
+ return wrapped(*args, **kwargs)
+ return positional_wrapper
+
+ if isinstance(max_positional_args, six.integer_types):
+ return positional_decorator
+ else:
+ args, _, _, defaults = inspect.getargspec(max_positional_args)
+ return positional(len(args) - len(defaults))(max_positional_args)
+
+
+def scopes_to_string(scopes):
+ """Converts scope value to a string.
+
+ If scopes is a string then it is simply passed through. If scopes is an
+ iterable then a string is returned that is all the individual scopes
+ concatenated with spaces.
+
+ Args:
+ scopes: string or iterable of strings, the scopes.
+
+ Returns:
+ The scopes formatted as a single string.
+ """
+ if isinstance(scopes, six.string_types):
+ return scopes
+ else:
+ return ' '.join(scopes)
+
+
+def string_to_scopes(scopes):
+ """Converts stringifed scope value to a list.
+
+ If scopes is a list then it is simply passed through. If scopes is an
+ string then a list of each individual scope is returned.
+
+ Args:
+ scopes: a string or iterable of strings, the scopes.
+
+ Returns:
+ The scopes in a list.
+ """
+ if not scopes:
+ return []
+ if isinstance(scopes, six.string_types):
+ return scopes.split(' ')
+ else:
+ return scopes
+
+
+def _add_query_parameter(url, name, value):
+ """Adds a query parameter to a url.
+
+ Replaces the current value if it already exists in the URL.
+
+ Args:
+ url: string, url to add the query parameter to.
+ name: string, query parameter name.
+ value: string, query parameter value.
+
+ Returns:
+ Updated query parameter. Does not update the url if value is None.
+ """
+ if value is None:
+ return url
+ else:
+ parsed = list(urllib.parse.urlparse(url))
+ q = dict(urllib.parse.parse_qsl(parsed[4]))
+ q[name] = value
+ parsed[4] = urllib.parse.urlencode(q)
+ return urllib.parse.urlunparse(parsed)