upgrade to oauth2client 3.0. Override some strings in GAM

to reduce custom changes to oauth2client.
This commit is contained in:
Jay Lee
2016-08-20 16:25:49 -04:00
parent f66aa71ec1
commit 219509853f
38 changed files with 2284 additions and 1017 deletions

View File

@ -40,6 +40,31 @@ import oauth2client.service_account
import oauth2client.file import oauth2client.file
import oauth2client.tools import oauth2client.tools
# Override some oauth2client.tools strings saving us a few GAM-specific mods to oauth2client
oauth2client.tools._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 nobrowser.txt and continuing with
authorization.
"""
oauth2client.tools._BROWSER_OPENED_MESSAGE = """
Your browser has been opened to visit:
{address}
If your browser is on a different machine then press CTRL+C and
create a file called nobrowser.txt in the same folder as GAM.
"""
oauth2client.tools._GO_TO_LINK_MESSAGE = """
Go to the following link in your browser:
{address}
"""
GAM_URL = u'http://git.io/gam' GAM_URL = u'http://git.io/gam'
GAM_INFO = u'GAM {0} - {1} / {2} / Python {3}.{4}.{5} {6} / {7} {8} /'.format(__version__, GAM_URL, GAM_INFO = u'GAM {0} - {1} / {2} / Python {3}.{4}.{5} {6} / {7} {8} /'.format(__version__, GAM_URL,
__author__, __author__,
@ -10221,7 +10246,7 @@ found at:
%s %s
with information from the APIs Console <https://cloud.google.com/console>. with information from the APIs Console <https://console.cloud.google.com>.
See: See:

View File

@ -14,7 +14,7 @@
"""Client library for using OAuth2, especially with Google APIs.""" """Client library for using OAuth2, especially with Google APIs."""
__version__ = '2.0.1' __version__ = '3.0.0'
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth' GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code' GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'

View File

@ -15,6 +15,7 @@
import base64 import base64
import json import json
import six import six
@ -67,7 +68,7 @@ def _to_bytes(value, encoding='ascii'):
if isinstance(result, six.binary_type): if isinstance(result, six.binary_type):
return result return result
else: else:
raise ValueError('%r could not be converted to bytes' % (value,)) raise ValueError('{0!r} could not be converted to bytes'.format(value))
def _from_bytes(value): def _from_bytes(value):
@ -88,7 +89,8 @@ def _from_bytes(value):
if isinstance(result, six.text_type): if isinstance(result, six.text_type):
return result return result
else: else:
raise ValueError('%r could not be converted to unicode' % (value,)) raise ValueError(
'{0!r} could not be converted to unicode'.format(value))
def _urlsafe_b64encode(raw_bytes): def _urlsafe_b64encode(raw_bytes):

View File

@ -13,12 +13,9 @@
# limitations under the License. # limitations under the License.
"""OpenSSL Crypto-related routines for oauth2client.""" """OpenSSL Crypto-related routines for oauth2client."""
import base64
from OpenSSL import crypto from OpenSSL import crypto
from oauth2client._helpers import _parse_pem_key from oauth2client import _helpers
from oauth2client._helpers import _to_bytes
class OpenSSLVerifier(object): class OpenSSLVerifier(object):
@ -45,8 +42,8 @@ class OpenSSLVerifier(object):
True if message was signed by the private key associated with the True if message was signed by the private key associated with the
public key that this object was constructed with. public key that this object was constructed with.
""" """
message = _to_bytes(message, encoding='utf-8') message = _helpers._to_bytes(message, encoding='utf-8')
signature = _to_bytes(signature, encoding='utf-8') signature = _helpers._to_bytes(signature, encoding='utf-8')
try: try:
crypto.verify(self._pubkey, signature, message, 'sha256') crypto.verify(self._pubkey, signature, message, 'sha256')
return True return True
@ -68,7 +65,7 @@ class OpenSSLVerifier(object):
Raises: Raises:
OpenSSL.crypto.Error: if the key_pem can't be parsed. OpenSSL.crypto.Error: if the key_pem can't be parsed.
""" """
key_pem = _to_bytes(key_pem) key_pem = _helpers._to_bytes(key_pem)
if is_x509_cert: if is_x509_cert:
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem) pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
else: else:
@ -96,7 +93,7 @@ class OpenSSLSigner(object):
Returns: Returns:
string, The signature of the message for the given key. string, The signature of the message for the given key.
""" """
message = _to_bytes(message, encoding='utf-8') message = _helpers._to_bytes(message, encoding='utf-8')
return crypto.sign(self._key, message, 'sha256') return crypto.sign(self._key, message, 'sha256')
@staticmethod @staticmethod
@ -113,12 +110,12 @@ class OpenSSLSigner(object):
Raises: Raises:
OpenSSL.crypto.Error if the key can't be parsed. OpenSSL.crypto.Error if the key can't be parsed.
""" """
key = _to_bytes(key) key = _helpers._to_bytes(key)
parsed_pem_key = _parse_pem_key(key) parsed_pem_key = _helpers._parse_pem_key(key)
if parsed_pem_key: if parsed_pem_key:
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
else: else:
password = _to_bytes(password, encoding='utf-8') password = _helpers._to_bytes(password, encoding='utf-8')
pkey = crypto.load_pkcs12(key, password).get_privatekey() pkey = crypto.load_pkcs12(key, password).get_privatekey()
return OpenSSLSigner(pkey) return OpenSSLSigner(pkey)
@ -133,7 +130,7 @@ def pkcs12_key_as_pem(private_key_bytes, private_key_password):
Returns: Returns:
String. PEM contents of ``private_key_bytes``. String. PEM contents of ``private_key_bytes``.
""" """
private_key_password = _to_bytes(private_key_password) private_key_password = _helpers._to_bytes(private_key_password)
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password) pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
return crypto.dump_privatekey(crypto.FILETYPE_PEM, return crypto.dump_privatekey(crypto.FILETYPE_PEM,
pkcs12.get_privatekey()) pkcs12.get_privatekey())

View File

@ -26,8 +26,7 @@ from pyasn1_modules.rfc5208 import PrivateKeyInfo
import rsa import rsa
import six import six
from oauth2client._helpers import _from_bytes from oauth2client import _helpers
from oauth2client._helpers import _to_bytes
_PKCS12_ERROR = r"""\ _PKCS12_ERROR = r"""\
@ -86,7 +85,7 @@ class RsaVerifier(object):
True if message was signed by the private key associated with the True if message was signed by the private key associated with the
public key that this object was constructed with. public key that this object was constructed with.
""" """
message = _to_bytes(message, encoding='utf-8') message = _helpers._to_bytes(message, encoding='utf-8')
try: try:
return rsa.pkcs1.verify(message, signature, self._pubkey) return rsa.pkcs1.verify(message, signature, self._pubkey)
except (ValueError, rsa.pkcs1.VerificationError): except (ValueError, rsa.pkcs1.VerificationError):
@ -111,7 +110,7 @@ class RsaVerifier(object):
"-----BEGIN CERTIFICATE-----" error, otherwise fails "-----BEGIN CERTIFICATE-----" error, otherwise fails
to find "-----BEGIN RSA PUBLIC KEY-----". to find "-----BEGIN RSA PUBLIC KEY-----".
""" """
key_pem = _to_bytes(key_pem) key_pem = _helpers._to_bytes(key_pem)
if is_x509_cert: if is_x509_cert:
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE') der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate()) asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
@ -145,7 +144,7 @@ class RsaSigner(object):
Returns: Returns:
string, The signature of the message for the given key. string, The signature of the message for the given key.
""" """
message = _to_bytes(message, encoding='utf-8') message = _helpers._to_bytes(message, encoding='utf-8')
return rsa.pkcs1.sign(message, self._key, 'SHA-256') return rsa.pkcs1.sign(message, self._key, 'SHA-256')
@classmethod @classmethod
@ -164,7 +163,7 @@ class RsaSigner(object):
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
PEM format. PEM format.
""" """
key = _from_bytes(key) # pem expects str in Py3 key = _helpers._from_bytes(key) # pem expects str in Py3
marker_id, key_bytes = pem.readPemBlocksFromFile( marker_id, key_bytes = pem.readPemBlocksFromFile(
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER) six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)

View File

@ -13,14 +13,12 @@
# limitations under the License. # limitations under the License.
"""pyCrypto Crypto-related routines for oauth2client.""" """pyCrypto Crypto-related routines for oauth2client."""
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256 from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5 from Crypto.Signature import PKCS1_v1_5
from Crypto.Util.asn1 import DerSequence from Crypto.Util.asn1 import DerSequence
from oauth2client._helpers import _parse_pem_key from oauth2client import _helpers
from oauth2client._helpers import _to_bytes
from oauth2client._helpers import _urlsafe_b64decode
class PyCryptoVerifier(object): class PyCryptoVerifier(object):
@ -47,7 +45,7 @@ class PyCryptoVerifier(object):
True if message was signed by the private key associated with the True if message was signed by the private key associated with the
public key that this object was constructed with. public key that this object was constructed with.
""" """
message = _to_bytes(message, encoding='utf-8') message = _helpers._to_bytes(message, encoding='utf-8')
return PKCS1_v1_5.new(self._pubkey).verify( return PKCS1_v1_5.new(self._pubkey).verify(
SHA256.new(message), signature) SHA256.new(message), signature)
@ -64,9 +62,9 @@ class PyCryptoVerifier(object):
Verifier instance. Verifier instance.
""" """
if is_x509_cert: if is_x509_cert:
key_pem = _to_bytes(key_pem) key_pem = _helpers._to_bytes(key_pem)
pemLines = key_pem.replace(b' ', b'').split() pemLines = key_pem.replace(b' ', b'').split()
certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1])) certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
certSeq = DerSequence() certSeq = DerSequence()
certSeq.decode(certDer) certSeq.decode(certDer)
tbsSeq = DerSequence() tbsSeq = DerSequence()
@ -97,7 +95,7 @@ class PyCryptoSigner(object):
Returns: Returns:
string, The signature of the message for the given key. string, The signature of the message for the given key.
""" """
message = _to_bytes(message, encoding='utf-8') message = _helpers._to_bytes(message, encoding='utf-8')
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
@staticmethod @staticmethod
@ -115,7 +113,7 @@ class PyCryptoSigner(object):
Raises: Raises:
NotImplementedError if the key isn't in PEM format. NotImplementedError if the key isn't in PEM format.
""" """
parsed_pem_key = _parse_pem_key(_to_bytes(key)) parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
if parsed_pem_key: if parsed_pem_key:
pkey = RSA.importKey(parsed_pem_key) pkey = RSA.importKey(parsed_pem_key)
else: else:

View File

@ -17,32 +17,25 @@
Tools for interacting with OAuth 2.0 protected resources. Tools for interacting with OAuth 2.0 protected resources.
""" """
import base64
import collections import collections
import copy import copy
import datetime import datetime
import json import json
import logging import logging
import os import os
import shutil
import socket import socket
import sys import sys
import tempfile import tempfile
import time
import shutil
import six import six
from six.moves import http_client from six.moves import http_client
from six.moves import urllib from six.moves import urllib
import httplib2 import oauth2client
from oauth2client import GOOGLE_AUTH_URI from oauth2client import _helpers
from oauth2client import GOOGLE_DEVICE_URI
from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import GOOGLE_TOKEN_INFO_URI
from oauth2client._helpers import _from_bytes
from oauth2client._helpers import _to_bytes
from oauth2client._helpers import _urlsafe_b64decode
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client import util from oauth2client import util
@ -53,9 +46,8 @@ HAS_CRYPTO = False
try: try:
from oauth2client import crypt from oauth2client import crypt
HAS_CRYPTO = True HAS_CRYPTO = True
if crypt.OpenSSLVerifier is not None: HAS_OPENSSL = crypt.OpenSSLVerifier is not None
HAS_OPENSSL = True except ImportError: # pragma: NO COVER
except ImportError:
pass pass
@ -73,9 +65,6 @@ ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
# Constant to use for the out of band OAuth 2.0 flow. # Constant to use for the out of band OAuth 2.0 flow.
OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
# Google Data client libraries may need to set this to [401, 403].
REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
# The value representing user credentials. # The value representing user credentials.
AUTHORIZED_USER = 'authorized_user' AUTHORIZED_USER = 'authorized_user'
@ -113,6 +102,14 @@ DEFAULT_ENV_NAME = 'UNKNOWN'
# If set to True _get_environment avoid GCE check (_detect_gce_environment) # If set to True _get_environment avoid GCE check (_detect_gce_environment)
NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') 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' _SERVER_SOFTWARE = 'SERVER_SOFTWARE'
_GCE_METADATA_HOST = '169.254.169.254' _GCE_METADATA_HOST = '169.254.169.254'
_METADATA_FLAVOR_HEADER = 'Metadata-Flavor' _METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
@ -122,6 +119,12 @@ _DESIRED_METADATA_FLAVOR = 'Google'
# easier testing (by replacing with a stub). # easier testing (by replacing with a stub).
_UTCNOW = datetime.datetime.utcnow _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): class SETTINGS(object):
"""Settings namespace for globally defined values.""" """Settings namespace for globally defined values."""
@ -179,26 +182,6 @@ class CryptoUnavailableError(Error, NotImplementedError):
"""Raised when a crypto library is required, but none is available.""" """Raised when a crypto library is required, but none is available."""
def _abstract():
raise NotImplementedError('You need to override this function')
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 _parse_expiry(expiry): def _parse_expiry(expiry):
if expiry and isinstance(expiry, datetime.datetime): if expiry and isinstance(expiry, datetime.datetime):
return expiry.strftime(EXPIRY_FORMAT) return expiry.strftime(EXPIRY_FORMAT)
@ -229,7 +212,7 @@ class Credentials(object):
http: httplib2.Http, an http object to be used to make the refresh http: httplib2.Http, an http object to be used to make the refresh
request. request.
""" """
_abstract() raise NotImplementedError
def refresh(self, http): def refresh(self, http):
"""Forces a refresh of the access_token. """Forces a refresh of the access_token.
@ -238,7 +221,7 @@ class Credentials(object):
http: httplib2.Http, an http object to be used to make the refresh http: httplib2.Http, an http object to be used to make the refresh
request. request.
""" """
_abstract() raise NotImplementedError
def revoke(self, http): def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void. """Revokes a refresh_token and makes the credentials void.
@ -247,7 +230,7 @@ class Credentials(object):
http: httplib2.Http, an http object to be used to make the revoke http: httplib2.Http, an http object to be used to make the revoke
request. request.
""" """
_abstract() raise NotImplementedError
def apply(self, headers): def apply(self, headers):
"""Add the authorization to the headers. """Add the authorization to the headers.
@ -255,7 +238,7 @@ class Credentials(object):
Args: Args:
headers: dict, the headers to add the Authorization header to. headers: dict, the headers to add the Authorization header to.
""" """
_abstract() raise NotImplementedError
def _to_json(self, strip, to_serialize=None): def _to_json(self, strip, to_serialize=None):
"""Utility function that creates JSON repr. of a Credentials object. """Utility function that creates JSON repr. of a Credentials object.
@ -264,8 +247,8 @@ class Credentials(object):
strip: array, An array of names of members to exclude from the strip: array, An array of names of members to exclude from the
JSON. JSON.
to_serialize: dict, (Optional) The properties for this object to_serialize: dict, (Optional) The properties for this object
that will be serialized. This allows callers to modify that will be serialized. This allows callers to
before serializing. modify before serializing.
Returns: Returns:
string, a JSON representation of this instance, suitable to pass to string, a JSON representation of this instance, suitable to pass to
@ -274,6 +257,9 @@ class Credentials(object):
curr_type = self.__class__ curr_type = self.__class__
if to_serialize is None: if to_serialize is None:
to_serialize = copy.copy(self.__dict__) 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: for member in strip:
if member in to_serialize: if member in to_serialize:
del to_serialize[member] del to_serialize[member]
@ -311,7 +297,7 @@ class Credentials(object):
An instance of the subclass of Credentials that was serialized with An instance of the subclass of Credentials that was serialized with
to_json(). to_json().
""" """
json_data_as_unicode = _from_bytes(json_data) json_data_as_unicode = _helpers._from_bytes(json_data)
data = json.loads(json_data_as_unicode) data = json.loads(json_data_as_unicode)
# Find and call the right classmethod from_json() to restore # Find and call the right classmethod from_json() to restore
# the object. # the object.
@ -361,7 +347,8 @@ class Storage(object):
Args: Args:
lock: An optional threading.Lock-like object. Must implement at lock: An optional threading.Lock-like object. Must implement at
least acquire() and release(). Does not need to be re-entrant. least acquire() and release(). Does not need to be
re-entrant.
""" """
self._lock = lock self._lock = lock
@ -390,7 +377,7 @@ class Storage(object):
Returns: Returns:
oauth2client.client.Credentials oauth2client.client.Credentials
""" """
_abstract() raise NotImplementedError
def locked_put(self, credentials): def locked_put(self, credentials):
"""Write a credential. """Write a credential.
@ -400,14 +387,14 @@ class Storage(object):
Args: Args:
credentials: Credentials, the credentials to store. credentials: Credentials, the credentials to store.
""" """
_abstract() raise NotImplementedError
def locked_delete(self): def locked_delete(self):
"""Delete a credential. """Delete a credential.
The Storage lock must be held when this is called. The Storage lock must be held when this is called.
""" """
_abstract() raise NotImplementedError
def get(self): def get(self):
"""Retrieve credential. """Retrieve credential.
@ -453,32 +440,6 @@ class Storage(object):
self.release_lock() self.release_lock()
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:
raise NonAsciiHeaderError(k, ': ', v)
return clean
def _update_query_params(uri, params): def _update_query_params(uri, params):
"""Updates a URI with new query parameters. """Updates a URI with new query parameters.
@ -586,67 +547,7 @@ class OAuth2Credentials(Credentials):
that adds in the Authorization header and then calls the original that adds in the Authorization header and then calls the original
version of 'request()'. version of 'request()'.
""" """
request_orig = http.request transport.wrap_http_for_auth(self, http)
# 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 self.access_token:
logger.info('Attempting refresh to obtain '
'initial access_token')
self._refresh(request_orig)
# Clone and modify the request headers to add the appropriate
# Authorization header.
if headers is None:
headers = {}
else:
headers = dict(headers)
self.apply(headers)
if self.user_agent is not None:
if 'user-agent' in headers:
headers['user-agent'] = (self.user_agent + ' ' +
headers['user-agent'])
else:
headers['user-agent'] = self.user_agent
body_stream_position = None
if all(getattr(body, stream_prop, None) for stream_prop in
('read', 'seek', 'tell')):
body_stream_position = body.tell()
resp, content = request_orig(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)
self._refresh(request_orig)
self.apply(headers)
if body_stream_position is not None:
body.seek(body_stream_position)
resp, content = request_orig(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', self)
return http return http
def refresh(self, http): def refresh(self, http):
@ -721,7 +622,7 @@ class OAuth2Credentials(Credentials):
Returns: Returns:
An instance of a Credentials subclass. An instance of a Credentials subclass.
""" """
data = json.loads(_from_bytes(json_data)) data = json.loads(_helpers._from_bytes(json_data))
if (data.get('token_expiry') and if (data.get('token_expiry') and
not isinstance(data['token_expiry'], datetime.datetime)): not isinstance(data['token_expiry'], datetime.datetime)):
try: try:
@ -772,7 +673,7 @@ class OAuth2Credentials(Credentials):
""" """
if not self.access_token or self.access_token_expired: if not self.access_token or self.access_token_expired:
if not http: if not http:
http = httplib2.Http() http = transport.get_http_object()
self.refresh(http) self.refresh(http)
return AccessTokenInfo(access_token=self.access_token, return AccessTokenInfo(access_token=self.access_token,
expires_in=self._expires_in()) expires_in=self._expires_in())
@ -894,7 +795,7 @@ class OAuth2Credentials(Credentials):
logger.info('Refreshing access_token') logger.info('Refreshing access_token')
resp, content = http_request( resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers) self.token_uri, method='POST', body=body, headers=headers)
content = _from_bytes(content) content = _helpers._from_bytes(content)
if resp.status == http_client.OK: if resp.status == http_client.OK:
d = json.loads(content) d = json.loads(content)
self.token_response = d self.token_response = d
@ -918,7 +819,7 @@ class OAuth2Credentials(Credentials):
# An {'error':...} response body means the token is expired or # An {'error':...} response body means the token is expired or
# revoked, so we flag the credentials as such. # revoked, so we flag the credentials as such.
logger.info('Failed to retrieve access token: %s', content) logger.info('Failed to retrieve access token: %s', content)
error_msg = 'Invalid response %s.' % resp['status'] error_msg = 'Invalid response {0}.'.format(resp['status'])
try: try:
d = json.loads(content) d = json.loads(content)
if 'error' in d: if 'error' in d:
@ -926,7 +827,7 @@ class OAuth2Credentials(Credentials):
if 'error_description' in d: if 'error_description' in d:
error_msg += ': ' + d['error_description'] error_msg += ': ' + d['error_description']
self.invalid = True self.invalid = True
if self.store: if self.store is not None:
self.store.locked_put(self) self.store.locked_put(self)
except (TypeError, ValueError): except (TypeError, ValueError):
pass pass
@ -963,9 +864,9 @@ class OAuth2Credentials(Credentials):
if resp.status == http_client.OK: if resp.status == http_client.OK:
self.invalid = True self.invalid = True
else: else:
error_msg = 'Invalid response %s.' % resp.status error_msg = 'Invalid response {0}.'.format(resp.status)
try: try:
d = json.loads(_from_bytes(content)) d = json.loads(_helpers._from_bytes(content))
if 'error' in d: if 'error' in d:
error_msg = d['error'] error_msg = d['error']
except (TypeError, ValueError): except (TypeError, ValueError):
@ -1004,12 +905,12 @@ class OAuth2Credentials(Credentials):
token_info_uri = _update_query_params(self.token_info_uri, token_info_uri = _update_query_params(self.token_info_uri,
query_params) query_params)
resp, content = http_request(token_info_uri) resp, content = http_request(token_info_uri)
content = _from_bytes(content) content = _helpers._from_bytes(content)
if resp.status == http_client.OK: if resp.status == http_client.OK:
d = json.loads(content) d = json.loads(content)
self.scopes = set(util.string_to_scopes(d.get('scope', ''))) self.scopes = set(util.string_to_scopes(d.get('scope', '')))
else: else:
error_msg = 'Invalid response %s.' % (resp.status,) error_msg = 'Invalid response {0}.'.format(resp.status)
try: try:
d = json.loads(content) d = json.loads(content)
if 'error_description' in d: if 'error_description' in d:
@ -1070,7 +971,7 @@ class AccessTokenCredentials(OAuth2Credentials):
@classmethod @classmethod
def from_json(cls, json_data): def from_json(cls, json_data):
data = json.loads(_from_bytes(json_data)) data = json.loads(_helpers._from_bytes(json_data))
retval = AccessTokenCredentials( retval = AccessTokenCredentials(
data['access_token'], data['access_token'],
data['user_agent']) data['user_agent'])
@ -1105,7 +1006,7 @@ def _detect_gce_environment():
# the metadata resolution was particularly slow. The latter case is # the metadata resolution was particularly slow. The latter case is
# "unlikely". # "unlikely".
connection = six.moves.http_client.HTTPConnection( connection = six.moves.http_client.HTTPConnection(
_GCE_METADATA_HOST, timeout=1) _GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT)
try: try:
headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
@ -1186,14 +1087,14 @@ class GoogleCredentials(OAuth2Credentials):
print(response) print(response)
""" """
NON_SERIALIZED_MEMBERS = ( NON_SERIALIZED_MEMBERS = (
frozenset(['_private_key']) | frozenset(['_private_key']) |
OAuth2Credentials.NON_SERIALIZED_MEMBERS) OAuth2Credentials.NON_SERIALIZED_MEMBERS)
"""Members that aren't serialized when object is converted to JSON.""" """Members that aren't serialized when object is converted to JSON."""
def __init__(self, access_token, client_id, client_secret, refresh_token, def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, token_expiry, token_uri, user_agent,
revoke_uri=GOOGLE_REVOKE_URI): revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Create an instance of GoogleCredentials. """Create an instance of GoogleCredentials.
This constructor is not usually called by the user, instead This constructor is not usually called by the user, instead
@ -1211,8 +1112,8 @@ class GoogleCredentials(OAuth2Credentials):
user_agent: string, The HTTP User-Agent to provide for this user_agent: string, The HTTP User-Agent to provide for this
application. application.
revoke_uri: string, URI for revoke endpoint. Defaults to revoke_uri: string, URI for revoke endpoint. Defaults to
GOOGLE_REVOKE_URI; a token can't be revoked if this oauth2client.GOOGLE_REVOKE_URI; a token can't be
is None. revoked if this is None.
""" """
super(GoogleCredentials, self).__init__( super(GoogleCredentials, self).__init__(
access_token, client_id, client_secret, refresh_token, access_token, client_id, client_secret, refresh_token,
@ -1237,14 +1138,17 @@ class GoogleCredentials(OAuth2Credentials):
def from_json(cls, json_data): def from_json(cls, json_data):
# TODO(issue 388): eliminate the circularity that is the reason for # TODO(issue 388): eliminate the circularity that is the reason for
# this non-top-level import. # this non-top-level import.
from oauth2client.service_account import ServiceAccountCredentials from oauth2client import service_account
data = json.loads(_from_bytes(json_data)) data = json.loads(_helpers._from_bytes(json_data))
# We handle service_account.ServiceAccountCredentials since it is a # We handle service_account.ServiceAccountCredentials since it is a
# possible return type of GoogleCredentials.get_application_default() # possible return type of GoogleCredentials.get_application_default()
if (data['_module'] == 'oauth2client.service_account' and if (data['_module'] == 'oauth2client.service_account' and
data['_class'] == 'ServiceAccountCredentials'): data['_class'] == 'ServiceAccountCredentials'):
return ServiceAccountCredentials.from_json(data) 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')) token_expiry = _parse_expiry(data.get('token_expiry'))
google_credentials = cls( google_credentials = cls(
@ -1348,10 +1252,10 @@ class GoogleCredentials(OAuth2Credentials):
"""Gets credentials implicitly from the environment. """Gets credentials implicitly from the environment.
Checks environment in order of precedence: Checks environment in order of precedence:
- Google App Engine (production and testing)
- Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to
a file with stored credentials information. a file with stored credentials information.
- Stored "well known" file associated with `gcloud` command line tool. - Stored "well known" file associated with `gcloud` command line tool.
- Google App Engine (production and testing)
- Google Compute Engine production environment. - Google Compute Engine production environment.
Raises: Raises:
@ -1360,8 +1264,8 @@ class GoogleCredentials(OAuth2Credentials):
""" """
# Environ checks (in order). # Environ checks (in order).
environ_checkers = [ environ_checkers = [
cls._implicit_credentials_from_gae,
cls._implicit_credentials_from_files, cls._implicit_credentials_from_files,
cls._implicit_credentials_from_gae,
cls._implicit_credentials_from_gce, cls._implicit_credentials_from_gce,
] ]
@ -1446,7 +1350,8 @@ def save_to_well_known_file(credentials, well_known_file=None):
config_dir = os.path.dirname(well_known_file) config_dir = os.path.dirname(well_known_file)
if not os.path.isdir(config_dir): if not os.path.isdir(config_dir):
raise OSError('Config directory does not exist: %s' % config_dir) raise OSError(
'Config directory does not exist: {0}'.format(config_dir))
credentials_data = credentials.serialization_data credentials_data = credentials.serialization_data
_save_private_file(well_known_file, credentials_data) _save_private_file(well_known_file, credentials_data)
@ -1454,8 +1359,7 @@ def save_to_well_known_file(credentials, well_known_file=None):
def _get_environment_variable_file(): def _get_environment_variable_file():
application_default_credential_filename = ( application_default_credential_filename = (
os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, None))
None))
if application_default_credential_filename: if application_default_credential_filename:
if os.path.isfile(application_default_credential_filename): if os.path.isfile(application_default_credential_filename):
@ -1521,11 +1425,11 @@ def _get_application_default_credential_from_file(filename):
client_secret=client_credentials['client_secret'], client_secret=client_credentials['client_secret'],
refresh_token=client_credentials['refresh_token'], refresh_token=client_credentials['refresh_token'],
token_expiry=None, token_expiry=None,
token_uri=GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
user_agent='Python client library') user_agent='Python client library')
else: # client_credentials['type'] == SERVICE_ACCOUNT else: # client_credentials['type'] == SERVICE_ACCOUNT
from oauth2client.service_account import ServiceAccountCredentials from oauth2client import service_account
return ServiceAccountCredentials.from_json_keyfile_dict( return service_account._JWTAccessCredentials.from_json_keyfile_dict(
client_credentials) client_credentials)
@ -1538,8 +1442,8 @@ def _raise_exception_for_reading_json(credential_file,
extra_help, extra_help,
error): error):
raise ApplicationDefaultCredentialsError( raise ApplicationDefaultCredentialsError(
'An error was encountered while reading json file: ' + 'An error was encountered while reading json file: ' +
credential_file + extra_help + ': ' + str(error)) credential_file + extra_help + ': ' + str(error))
def _get_application_default_credential_GAE(): def _get_application_default_credential_GAE():
@ -1567,8 +1471,8 @@ class AssertionCredentials(GoogleCredentials):
@util.positional(2) @util.positional(2)
def __init__(self, assertion_type, user_agent=None, def __init__(self, assertion_type, user_agent=None,
token_uri=GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
**unused_kwargs): **unused_kwargs):
"""Constructor for AssertionFlowCredentials. """Constructor for AssertionFlowCredentials.
@ -1605,7 +1509,7 @@ class AssertionCredentials(GoogleCredentials):
def _generate_assertion(self): def _generate_assertion(self):
"""Generate assertion string to be used in the access token request.""" """Generate assertion string to be used in the access token request."""
_abstract() raise NotImplementedError
def _revoke(self, http_request): def _revoke(self, http_request):
"""Revokes the access_token and deletes the store if available. """Revokes the access_token and deletes the store if available.
@ -1630,7 +1534,7 @@ class AssertionCredentials(GoogleCredentials):
raise NotImplementedError('This method is abstract.') raise NotImplementedError('This method is abstract.')
def _RequireCryptoOrDie(): def _require_crypto_or_die():
"""Ensure we have a crypto library, or throw CryptoUnavailableError. """Ensure we have a crypto library, or throw CryptoUnavailableError.
The oauth2client.crypt module requires either PyCrypto or PyOpenSSL The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
@ -1641,11 +1545,6 @@ def _RequireCryptoOrDie():
raise CryptoUnavailableError('No crypto library available') raise CryptoUnavailableError('No crypto library available')
# Only used in verify_id_token(), which is always calling to the same URI
# for the certs.
_cached_http = httplib2.Http(MemoryCache())
@util.positional(2) @util.positional(2)
def verify_id_token(id_token, audience, http=None, def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATION_CERTS): cert_uri=ID_TOKEN_VERIFICATION_CERTS):
@ -1669,16 +1568,16 @@ def verify_id_token(id_token, audience, http=None,
oauth2client.crypt.AppIdentityError: if the JWT fails to verify. oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
CryptoUnavailableError: if no crypto library is available. CryptoUnavailableError: if no crypto library is available.
""" """
_RequireCryptoOrDie() _require_crypto_or_die()
if http is None: if http is None:
http = _cached_http http = transport.get_cached_http()
resp, content = http.request(cert_uri) resp, content = http.request(cert_uri)
if resp.status == http_client.OK: if resp.status == http_client.OK:
certs = json.loads(_from_bytes(content)) certs = json.loads(_helpers._from_bytes(content))
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
else: else:
raise VerifyJwtTokenError('Status code: %d' % resp.status) raise VerifyJwtTokenError('Status code: {0}'.format(resp.status))
def _extract_id_token(id_token): def _extract_id_token(id_token):
@ -1699,9 +1598,10 @@ def _extract_id_token(id_token):
if len(segments) != 3: if len(segments) != 3:
raise VerifyJwtTokenError( raise VerifyJwtTokenError(
'Wrong number of segments in token: %s' % id_token) 'Wrong number of segments in token: {0}'.format(id_token))
return json.loads(_from_bytes(_urlsafe_b64decode(segments[1]))) return json.loads(
_helpers._from_bytes(_helpers._urlsafe_b64decode(segments[1])))
def _parse_exchange_token_response(content): def _parse_exchange_token_response(content):
@ -1718,7 +1618,7 @@ def _parse_exchange_token_response(content):
i.e. {}. That basically indicates a failure. i.e. {}. That basically indicates a failure.
""" """
resp = {} resp = {}
content = _from_bytes(content) content = _helpers._from_bytes(content)
try: try:
resp = json.loads(content) resp = json.loads(content)
except Exception: except Exception:
@ -1736,11 +1636,12 @@ def _parse_exchange_token_response(content):
@util.positional(4) @util.positional(4)
def credentials_from_code(client_id, client_secret, scope, code, def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None, redirect_uri='postmessage', http=None,
user_agent=None, token_uri=GOOGLE_TOKEN_URI, user_agent=None,
auth_uri=GOOGLE_AUTH_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, auth_uri=oauth2client.GOOGLE_AUTH_URI,
device_uri=GOOGLE_DEVICE_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
token_info_uri=GOOGLE_TOKEN_INFO_URI): device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI):
"""Exchanges an authorization code for an OAuth2Credentials object. """Exchanges an authorization code for an OAuth2Credentials object.
Args: Args:
@ -1864,11 +1765,38 @@ class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
}) })
if 'expires_in' in response: if 'expires_in' in response:
kwargs['user_code_expiry'] = ( kwargs['user_code_expiry'] = (
datetime.datetime.now() + _UTCNOW() +
datetime.timedelta(seconds=int(response['expires_in']))) datetime.timedelta(seconds=int(response['expires_in'])))
return cls(**kwargs) 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): class OAuth2WebServerFlow(Flow):
"""Does the Web Server Flow for OAuth 2.0. """Does the Web Server Flow for OAuth 2.0.
@ -1881,18 +1809,18 @@ class OAuth2WebServerFlow(Flow):
scope=None, scope=None,
redirect_uri=None, redirect_uri=None,
user_agent=None, user_agent=None,
auth_uri=GOOGLE_AUTH_URI, auth_uri=oauth2client.GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
login_hint=None, login_hint=None,
device_uri=GOOGLE_DEVICE_URI, device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=GOOGLE_TOKEN_INFO_URI, token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
authorization_header=None, authorization_header=None,
**kwargs): **kwargs):
"""Constructor for OAuth2WebServerFlow. """Constructor for OAuth2WebServerFlow.
The kwargs argument is used to set extra query parameters on the The kwargs argument is used to set extra query parameters on the
auth_uri. For example, the access_type and approval_prompt auth_uri. For example, the access_type and prompt
query parameters can be set via kwargs. query parameters can be set via kwargs.
Args: Args:
@ -1944,11 +1872,7 @@ class OAuth2WebServerFlow(Flow):
self.device_uri = device_uri self.device_uri = device_uri
self.token_info_uri = token_info_uri self.token_info_uri = token_info_uri
self.authorization_header = authorization_header self.authorization_header = authorization_header
self.params = { self.params = _oauth2_web_server_flow_params(kwargs)
'access_type': 'offline',
'response_type': 'code',
}
self.params.update(kwargs)
@util.positional(1) @util.positional(1)
def step1_get_authorize_url(self, redirect_uri=None, state=None): def step1_get_authorize_url(self, redirect_uri=None, state=None):
@ -2014,25 +1938,25 @@ class OAuth2WebServerFlow(Flow):
headers['user-agent'] = self.user_agent headers['user-agent'] = self.user_agent
if http is None: if http is None:
http = httplib2.Http() http = transport.get_http_object()
resp, content = http.request(self.device_uri, method='POST', body=body, resp, content = http.request(self.device_uri, method='POST', body=body,
headers=headers) headers=headers)
content = _from_bytes(content) content = _helpers._from_bytes(content)
if resp.status == http_client.OK: if resp.status == http_client.OK:
try: try:
flow_info = json.loads(content) flow_info = json.loads(content)
except ValueError as e: except ValueError as exc:
raise OAuth2DeviceCodeError( raise OAuth2DeviceCodeError(
'Could not parse server response as JSON: "%s", ' 'Could not parse server response as JSON: "{0}", '
'error: "%s"' % (content, e)) 'error: "{1}"'.format(content, exc))
return DeviceFlowInfo.FromResponse(flow_info) return DeviceFlowInfo.FromResponse(flow_info)
else: else:
error_msg = 'Invalid response %s.' % resp.status error_msg = 'Invalid response {0}.'.format(resp.status)
try: try:
d = json.loads(content) error_dict = json.loads(content)
if 'error' in d: if 'error' in error_dict:
error_msg += ' Error: %s' % d['error'] error_msg += ' Error: {0}'.format(error_dict['error'])
except ValueError: except ValueError:
# Couldn't decode a JSON response, stick with the # Couldn't decode a JSON response, stick with the
# default message. # default message.
@ -2069,7 +1993,7 @@ class OAuth2WebServerFlow(Flow):
if code is None: if code is None:
code = device_flow_info.device_code code = device_flow_info.device_code
elif not isinstance(code, six.string_types): elif not isinstance(code, (six.string_types, six.binary_type)):
if 'code' not in code: if 'code' not in code:
raise FlowExchangeError(code.get( raise FlowExchangeError(code.get(
'error', 'No code was supplied in the query parameters.')) 'error', 'No code was supplied in the query parameters.'))
@ -2097,7 +2021,7 @@ class OAuth2WebServerFlow(Flow):
headers['user-agent'] = self.user_agent headers['user-agent'] = self.user_agent
if http is None: if http is None:
http = httplib2.Http() http = transport.get_http_object()
resp, content = http.request(self.token_uri, method='POST', body=body, resp, content = http.request(self.token_uri, method='POST', body=body,
headers=headers) headers=headers)
@ -2108,7 +2032,7 @@ class OAuth2WebServerFlow(Flow):
if not refresh_token: if not refresh_token:
logger.info( logger.info(
'Received token response with no refresh_token. Consider ' 'Received token response with no refresh_token. Consider '
"reauthenticating with approval_prompt='force'.") "reauthenticating with prompt='consent'.")
token_expiry = None token_expiry = None
if 'expires_in' in d: if 'expires_in' in d:
delta = datetime.timedelta(seconds=int(d['expires_in'])) delta = datetime.timedelta(seconds=int(d['expires_in']))
@ -2132,7 +2056,7 @@ class OAuth2WebServerFlow(Flow):
error_msg = (str(d['error']) + error_msg = (str(d['error']) +
str(d.get('error_description', ''))) str(d.get('error_description', '')))
else: else:
error_msg = 'Invalid response: %s.' % str(resp.status) error_msg = 'Invalid response: {0}.'.format(str(resp.status))
raise FlowExchangeError(error_msg) raise FlowExchangeError(error_msg)
@ -2196,11 +2120,14 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
client_info['client_id'], client_info['client_secret'], client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs) scope, **constructor_kwargs)
except clientsecrets.InvalidClientSecretsError: except clientsecrets.InvalidClientSecretsError as e:
if message: if message is not None:
if e.args:
message = ('The client secrets were invalid: '
'\n{0}\n{1}'.format(e, message))
sys.exit(message) sys.exit(message)
else: else:
raise raise
else: else:
raise UnknownClientSecretsFlowError( raise UnknownClientSecretsFlowError(
'This OAuth 2.0 flow is unsupported: %r' % client_type) 'This OAuth 2.0 flow is unsupported: {0!r}'.format(client_type))

View File

@ -19,8 +19,8 @@ an OAuth 2.0 protected service.
""" """
import json import json
import six
import six
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
@ -93,17 +93,17 @@ def _validate_clientsecrets(clientsecrets_dict):
if client_type not in VALID_CLIENT: if client_type not in VALID_CLIENT:
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
'Unknown client type: %s.' % (client_type,)) 'Unknown client type: {0}.'.format(client_type))
for prop_name in VALID_CLIENT[client_type]['required']: for prop_name in VALID_CLIENT[client_type]['required']:
if prop_name not in client_info: if prop_name not in client_info:
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
'Missing property "%s" in a client type of "%s".' % 'Missing property "{0}" in a client type of "{1}".'.format(
(prop_name, client_type)) prop_name, client_type))
for prop_name in VALID_CLIENT[client_type]['string']: for prop_name in VALID_CLIENT[client_type]['string']:
if client_info[prop_name].startswith('[['): if client_info[prop_name].startswith('[['):
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
'Property "%s" is not configured.' % prop_name) 'Property "{0}" is not configured.'.format(prop_name))
return client_type, client_info return client_type, client_info

View File

@ -76,9 +76,9 @@ class FlowNDBProperty(ndb.PickleProperty):
""" """
_LOGGER.info('validate: Got type %s', type(value)) _LOGGER.info('validate: Got type %s', type(value))
if value is not None and not isinstance(value, client.Flow): if value is not None and not isinstance(value, client.Flow):
raise TypeError('Property %s must be convertible to a flow ' raise TypeError(
'instance; received: %s.' % (self._name, 'Property {0} must be convertible to a flow '
value)) 'instance; received: {1}.'.format(self._name, value))
class CredentialsNDBProperty(ndb.BlobProperty): class CredentialsNDBProperty(ndb.BlobProperty):
@ -104,9 +104,9 @@ class CredentialsNDBProperty(ndb.BlobProperty):
""" """
_LOGGER.info('validate: Got type %s', type(value)) _LOGGER.info('validate: Got type %s', type(value))
if value is not None and not isinstance(value, client.Credentials): if value is not None and not isinstance(value, client.Credentials):
raise TypeError('Property %s must be convertible to a ' raise TypeError(
'credentials instance; received: %s.' % 'Property {0} must be convertible to a credentials '
(self._name, value)) 'instance; received: {1}.'.format(self._name, value))
def _to_base_type(self, value): def _to_base_type(self, value):
"""Converts our validated value to a JSON serialized string. """Converts our validated value to a JSON serialized string.

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -24,26 +24,18 @@ import os
import pickle import pickle
import threading import threading
import httplib2
import webapp2 as webapp
from google.appengine.api import app_identity from google.appengine.api import app_identity
from google.appengine.api import memcache from google.appengine.api import memcache
from google.appengine.api import users from google.appengine.api import users
from google.appengine.ext import db from google.appengine.ext import db
from google.appengine.ext.webapp.util import login_required from google.appengine.ext.webapp.util import login_required
import httplib2
import webapp2 as webapp
from oauth2client import GOOGLE_AUTH_URI import oauth2client
from oauth2client import GOOGLE_REVOKE_URI from oauth2client import client
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client import util from oauth2client import util
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import AssertionCredentials
from oauth2client.client import Credentials
from oauth2client.client import Flow
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.client import Storage
from oauth2client.contrib import xsrfutil from oauth2client.contrib import xsrfutil
# This is a temporary fix for a Google internal issue. # This is a temporary fix for a Google internal issue.
@ -61,7 +53,7 @@ OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
XSRF_MEMCACHE_ID = 'xsrf_secret_key' XSRF_MEMCACHE_ID = 'xsrf_secret_key'
if _appengine_ndb is None: if _appengine_ndb is None: # pragma: NO COVER
CredentialsNDBModel = None CredentialsNDBModel = None
CredentialsNDBProperty = None CredentialsNDBProperty = None
FlowNDBProperty = None FlowNDBProperty = None
@ -89,14 +81,6 @@ def _safe_html(s):
return cgi.escape(s, quote=1).replace("'", '&#39;') return cgi.escape(s, quote=1).replace("'", '&#39;')
class InvalidClientSecretsError(Exception):
"""The client_secrets.json file is malformed or missing required fields."""
class InvalidXsrfTokenError(Exception):
"""The XSRF token is invalid or expired."""
class SiteXsrfSecretKey(db.Model): class SiteXsrfSecretKey(db.Model):
"""Storage for the sites XSRF secret key. """Storage for the sites XSRF secret key.
@ -134,7 +118,7 @@ def xsrf_secret_key():
return str(secret) return str(secret)
class AppAssertionCredentials(AssertionCredentials): class AppAssertionCredentials(client.AssertionCredentials):
"""Credentials object for App Engine Assertion Grants """Credentials object for App Engine Assertion Grants
This object will allow an App Engine application to identify itself to This object will allow an App Engine application to identify itself to
@ -193,7 +177,7 @@ class AppAssertionCredentials(AssertionCredentials):
(token, _) = app_identity.get_access_token( (token, _) = app_identity.get_access_token(
scopes, service_account_id=self.service_account_id) scopes, service_account_id=self.service_account_id)
except app_identity.Error as e: except app_identity.Error as e:
raise AccessTokenRefreshError(str(e)) raise client.AccessTokenRefreshError(str(e))
self.access_token = token self.access_token = token
@property @property
@ -244,7 +228,7 @@ class FlowProperty(db.Property):
""" """
# Tell what the user type is. # Tell what the user type is.
data_type = Flow data_type = client.Flow
# For writing to datastore. # For writing to datastore.
def get_value_for_datastore(self, model_instance): def get_value_for_datastore(self, model_instance):
@ -259,10 +243,10 @@ class FlowProperty(db.Property):
return pickle.loads(value) return pickle.loads(value)
def validate(self, value): def validate(self, value):
if value is not None and not isinstance(value, Flow): if value is not None and not isinstance(value, client.Flow):
raise db.BadValueError('Property %s must be convertible ' raise db.BadValueError(
'to a FlowThreeLegged instance (%s)' % 'Property {0} must be convertible '
(self.name, value)) 'to a FlowThreeLegged instance ({1})'.format(self.name, value))
return super(FlowProperty, self).validate(value) return super(FlowProperty, self).validate(value)
def empty(self, value): def empty(self, value):
@ -273,11 +257,11 @@ class CredentialsProperty(db.Property):
"""App Engine datastore Property for Credentials. """App Engine datastore Property for Credentials.
Utility property that allows easy storage and retrieval of Utility property that allows easy storage and retrieval of
oath2client.Credentials oauth2client.Credentials
""" """
# Tell what the user type is. # Tell what the user type is.
data_type = Credentials data_type = client.Credentials
# For writing to datastore. # For writing to datastore.
def get_value_for_datastore(self, model_instance): def get_value_for_datastore(self, model_instance):
@ -298,7 +282,7 @@ class CredentialsProperty(db.Property):
if len(value) == 0: if len(value) == 0:
return None return None
try: try:
credentials = Credentials.new_from_json(value) credentials = client.Credentials.new_from_json(value)
except ValueError: except ValueError:
credentials = None credentials = None
return credentials return credentials
@ -306,14 +290,14 @@ class CredentialsProperty(db.Property):
def validate(self, value): def validate(self, value):
value = super(CredentialsProperty, self).validate(value) value = super(CredentialsProperty, self).validate(value)
logger.info("validate: Got type " + str(type(value))) logger.info("validate: Got type " + str(type(value)))
if value is not None and not isinstance(value, Credentials): if value is not None and not isinstance(value, client.Credentials):
raise db.BadValueError('Property %s must be convertible ' raise db.BadValueError(
'to a Credentials instance (%s)' % 'Property {0} must be convertible '
(self.name, value)) 'to a Credentials instance ({1})'.format(self.name, value))
return value return value
class StorageByKeyName(Storage): class StorageByKeyName(client.Storage):
"""Store and retrieve a credential to and from the App Engine datastore. """Store and retrieve a credential to and from the App Engine datastore.
This Storage helper presumes the Credentials have been stored as a This Storage helper presumes the Credentials have been stored as a
@ -365,8 +349,8 @@ class StorageByKeyName(Storage):
elif issubclass(self._model, db.Model): elif issubclass(self._model, db.Model):
return False return False
raise TypeError('Model class not an NDB or DB model: %s.' % raise TypeError(
(self._model,)) 'Model class not an NDB or DB model: {0}.'.format(self._model))
def _get_entity(self): def _get_entity(self):
"""Retrieve entity from datastore. """Retrieve entity from datastore.
@ -405,7 +389,7 @@ class StorageByKeyName(Storage):
if self._cache: if self._cache:
json = self._cache.get(self._key_name) json = self._cache.get(self._key_name)
if json: if json:
credentials = Credentials.new_from_json(json) credentials = client.Credentials.new_from_json(json)
if credentials is None: if credentials is None:
entity = self._get_entity() entity = self._get_entity()
if entity is not None: if entity is not None:
@ -476,18 +460,15 @@ def _parse_state_value(state, user):
state: string, The value of the state parameter. state: string, The value of the state parameter.
user: google.appengine.api.users.User, The current user. user: google.appengine.api.users.User, The current user.
Raises:
InvalidXsrfTokenError: if the XSRF token is invalid.
Returns: Returns:
The redirect URI. The redirect URI, or None if XSRF token is not valid.
""" """
uri, token = state.rsplit(':', 1) uri, token = state.rsplit(':', 1)
if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
action_id=uri): action_id=uri):
raise InvalidXsrfTokenError() return uri
else:
return uri return None
class OAuth2Decorator(object): class OAuth2Decorator(object):
@ -544,9 +525,9 @@ class OAuth2Decorator(object):
@util.positional(4) @util.positional(4)
def __init__(self, client_id, client_secret, scope, def __init__(self, client_id, client_secret, scope,
auth_uri=GOOGLE_AUTH_URI, auth_uri=oauth2client.GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
user_agent=None, user_agent=None,
message=None, message=None,
callback_path='/oauth2callback', callback_path='/oauth2callback',
@ -665,7 +646,7 @@ class OAuth2Decorator(object):
return request_handler.redirect(self.authorize_url()) return request_handler.redirect(self.authorize_url())
try: try:
resp = method(request_handler, *args, **kwargs) resp = method(request_handler, *args, **kwargs)
except AccessTokenRefreshError: except client.AccessTokenRefreshError:
return request_handler.redirect(self.authorize_url()) return request_handler.redirect(self.authorize_url())
finally: finally:
self.credentials = None self.credentials = None
@ -686,7 +667,7 @@ class OAuth2Decorator(object):
if self.flow is None: if self.flow is None:
redirect_uri = request_handler.request.relative_url( redirect_uri = request_handler.request.relative_url(
self._callback_path) # Usually /oauth2callback self._callback_path) # Usually /oauth2callback
self.flow = OAuth2WebServerFlow( self.flow = client.OAuth2WebServerFlow(
self._client_id, self._client_secret, self._scope, self._client_id, self._client_secret, self._scope,
redirect_uri=redirect_uri, user_agent=self._user_agent, redirect_uri=redirect_uri, user_agent=self._user_agent,
auth_uri=self._auth_uri, token_uri=self._token_uri, auth_uri=self._auth_uri, token_uri=self._token_uri,
@ -802,8 +783,8 @@ class OAuth2Decorator(object):
if error: if error:
errormsg = self.request.get('error_description', error) errormsg = self.request.get('error_description', error)
self.response.out.write( self.response.out.write(
'The authorization request failed: %s' % 'The authorization request failed: {0}'.format(
_safe_html(errormsg)) _safe_html(errormsg)))
else: else:
user = users.get_current_user() user = users.get_current_user()
decorator._create_flow(self) decorator._create_flow(self)
@ -815,6 +796,10 @@ class OAuth2Decorator(object):
user=user).put(credentials) user=user).put(credentials)
redirect_uri = _parse_state_value( redirect_uri = _parse_state_value(
str(self.request.get('state')), user) 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 if (decorator._token_response_param and
credentials.token_response): credentials.token_response):
@ -885,7 +870,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
cache=cache) cache=cache)
if client_type not in (clientsecrets.TYPE_WEB, if client_type not in (clientsecrets.TYPE_WEB,
clientsecrets.TYPE_INSTALLED): clientsecrets.TYPE_INSTALLED):
raise InvalidClientSecretsError( raise clientsecrets.InvalidClientSecretsError(
"OAuth2Decorator doesn't support this OAuth 2.0 flow.") "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
constructor_kwargs = dict(kwargs) constructor_kwargs = dict(kwargs)

View File

@ -19,13 +19,9 @@ import json
import os import os
import socket import socket
from oauth2client._helpers import _to_bytes from oauth2client import _helpers
from oauth2client import client from oauth2client import client
# Expose utcnow() at module level to allow for
# easier testing (by replacing with a stub).
_UTCNOW = datetime.datetime.utcnow
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT' DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
@ -83,8 +79,8 @@ def _SendRecv():
sock.connect(('localhost', port)) sock.connect(('localhost', port))
data = CREDENTIAL_INFO_REQUEST_JSON data = CREDENTIAL_INFO_REQUEST_JSON
msg = '%s\n%s' % (len(data), data) msg = '{0}\n{1}'.format(len(data), data)
sock.sendall(_to_bytes(msg, encoding='utf-8')) sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
header = sock.recv(6).decode() header = sock.recv(6).decode()
if '\n' not in header: if '\n' not in header:
@ -127,7 +123,7 @@ class DevshellCredentials(client.GoogleCredentials):
expires_in = self.devshell_response.expires_in expires_in = self.devshell_response.expires_in
if expires_in is not None: if expires_in is not None:
delta = datetime.timedelta(seconds=expires_in) delta = datetime.timedelta(seconds=expires_in)
self.token_expiry = _UTCNOW() + delta self.token_expiry = client._UTCNOW() + delta
else: else:
self.token_expiry = None self.token_expiry = None

View File

@ -14,11 +14,10 @@
"""Dictionary storage for OAuth2 Credentials.""" """Dictionary storage for OAuth2 Credentials."""
from oauth2client.client import OAuth2Credentials from oauth2client import client
from oauth2client.client import Storage
class DictionaryStorage(Storage): class DictionaryStorage(client.Storage):
"""Store and retrieve credentials to and from a dictionary-like object. """Store and retrieve credentials to and from a dictionary-like object.
Args: Args:
@ -46,7 +45,7 @@ class DictionaryStorage(Storage):
if serialized is None: if serialized is None:
return None return None
credentials = OAuth2Credentials.from_json(serialized) credentials = client.OAuth2Credentials.from_json(serialized)
credentials.set_store(self) credentials.set_store(self)
return credentials return credentials

View File

@ -1,173 +0,0 @@
# 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.
"""OAuth 2.0 utilities for Django.
Utilities for using OAuth 2.0 in conjunction with
the Django datastore.
"""
import oauth2client
import base64
import pickle
import six
from django.db import models
from django.utils.encoding import smart_bytes, smart_text
from oauth2client.client import Storage as BaseStorage
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
class CredentialsField(six.with_metaclass(models.SubfieldBase, models.Field)):
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 'TextField'
def to_python(self, value):
if value is None:
return None
if isinstance(value, oauth2client.client.Credentials):
return value
return pickle.loads(base64.b64decode(smart_bytes(value)))
def get_prep_value(self, value):
if value is None:
return None
return 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)
class FlowField(six.with_metaclass(models.SubfieldBase, models.Field)):
def __init__(self, *args, **kwargs):
if 'null' not in kwargs:
kwargs['null'] = True
super(FlowField, self).__init__(*args, **kwargs)
def get_internal_type(self):
return 'TextField'
def to_python(self, value):
if value is None:
return None
if isinstance(value, oauth2client.client.Flow):
return value
return pickle.loads(base64.b64decode(value))
def get_prep_value(self, value):
if value is None:
return None
return 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)
class Storage(BaseStorage):
"""Store and retrieve a single credential to and from the Django datastore.
This Storage helper presumes the Credentials
have been stored as a CredenialsField
on a db model class.
"""
def __init__(self, model_class, key_name, key_value, property_name):
"""Constructor for Storage.
Args:
model: 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(Storage, 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.
Returns:
oauth2client.Credentials
"""
credential = None
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 credential and hasattr(credential, 'set_store'):
credential.set_store(self)
return credential
def locked_put(self, credentials, overwrite=False):
"""Write a Credentials to the Django datastore.
Args:
credentials: Credentials, the credentials to store.
overwrite: Boolean, indicates whether you would like these
credentials to overwrite any existing stored
credentials.
"""
args = {self.key_name: self.key_value}
if overwrite:
(entity,
unused_is_new) = self.model_class.objects.get_or_create(**args)
else:
entity = self.model_class(**args)
setattr(entity, self.property_name, credentials)
entity.save()
def locked_delete(self):
"""Delete Credentials from the datastore."""
query = {self.key_name: self.key_value}
entities = self.model_class.objects.filter(**query).delete()

View File

@ -12,17 +12,28 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Utilities for the Django web framework """Utilities for the Django web framework.
Provides Django views and helpers the make using the OAuth2 web server Provides Django views and helpers the make using the OAuth2 web server
flow easier. It includes an ``oauth_required`` decorator to automatically ensure flow easier. It includes an ``oauth_required`` decorator to automatically
that user credentials are available, and an ``oauth_enabled`` decorator to check ensure that user credentials are available, and an ``oauth_enabled`` decorator
if the user has authorized, and helper shortcuts to create the authorization to check if the user has authorized, and helper shortcuts to create the
URL otherwise. 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 Configuration
============= ===============
To configure, you'll need a set of OAuth2 web application credentials from 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>`. `Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
@ -35,9 +46,13 @@ Add the helper to your INSTALLED_APPS:
INSTALLED_APPS = ( INSTALLED_APPS = (
# other apps # other apps
"django.contrib.sessions.middleware"
"oauth2client.contrib.django_util" "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 Add the client secrets created earlier to the settings. You can either
specify the path to the credentials file in JSON format specify the path to the credentials file in JSON format
@ -88,8 +103,8 @@ Add the oauth2 routes to your application's urls.py urlpatterns.
urlpatterns += [url(r'^oauth2/', include(oauth2_urls))] urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
To require OAuth2 credentials for a view, use the `oauth2_required` decorator. 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 This creates a credentials object with an id_token, and allows you to create
`http` object to build service clients with. These are all attached to the an `http` object to build service clients with. These are all attached to the
request.oauth request.oauth
.. code-block:: python .. code-block:: python
@ -105,7 +120,10 @@ request.oauth
http=request.oauth.http, http=request.oauth.http,
developerKey=API_KEY) developerKey=API_KEY)
events = service.events().list(calendarId='primary').execute()['items'] events = service.events().list(calendarId='primary').execute()['items']
return HttpResponse("email: %s , calendar: %s" % (email, str(events))) 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. To make OAuth2 optional and provide an authorization link in your own views.
@ -120,11 +138,12 @@ To make OAuth2 optional and provide an authorization link in your own views.
if request.oauth.has_credentials(): if request.oauth.has_credentials():
# this could be passed into a view # this could be passed into a view
# request.oauth.http is also initialized # request.oauth.http is also initialized
return HttpResponse("User email: %s" return HttpResponse("User email: {0}".format(
% request.oauth.credentials.id_token['email']) request.oauth.credentials.id_token['email']))
else: else:
return HttpResponse('Here is an OAuth Authorize link: return HttpResponse(
<a href="%s">Authorize</a>' % request.oauth.get_authorize_redirect()) '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 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) the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
@ -143,8 +162,9 @@ and specify additional scopes in the decorator arguments.
events = service.files().list().execute()['items'] events = service.files().list().execute()['items']
return HttpResponse(str(events)) return HttpResponse(str(events))
else: else:
return HttpResponse('Here is an OAuth Authorize link: return HttpResponse(
<a href="%s">Authorize</a>' % request.oauth.get_authorize_redirect()) '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 To provide a callback on authorization being completed, use the
@ -157,26 +177,78 @@ oauth2_authorized signal:
from oauth2client.contrib.django_util.signals import oauth2_authorized from oauth2client.contrib.django_util.signals import oauth2_authorized
def test_callback(sender, request, credentials, **kwargs): def test_callback(sender, request, credentials, **kwargs):
print "Authorization Signal Received %s" % credentials.id_token['email'] print("Authorization Signal Received {0}".format(
credentials.id_token['email']))
oauth2_authorized.connect(test_callback) 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 import django.conf
from django.core import exceptions from django.core import exceptions
from django.core import urlresolvers from django.core import urlresolvers
import httplib2 import httplib2
from oauth2client import clientsecrets
from oauth2client.contrib.django_util import storage
from six.moves.urllib import parse 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_DEFAULT_SCOPES = ('email',)
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth' GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
def _load_client_secrets(filename): def _load_client_secrets(filename):
"""Loads client secrets from the given 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) client_type, client_info = clientsecrets.loadfile(filename)
if client_type != clientsecrets.TYPE_WEB: if client_type != clientsecrets.TYPE_WEB:
@ -187,8 +259,16 @@ def _load_client_secrets(filename):
def _get_oauth2_client_id_and_secret(settings_instance): def _get_oauth2_client_id_and_secret(settings_instance):
"""Initializes client id and client secret based on the settings""" """Initializes client id and client secret based on the settings.
secret_json = getattr(django.conf.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) 'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
if secret_json is not None: if secret_json is not None:
return _load_client_secrets(secret_json) return _load_client_secrets(secret_json)
@ -201,9 +281,36 @@ def _get_oauth2_client_id_and_secret(settings_instance):
return client_id, client_secret return client_id, client_secret
else: else:
raise exceptions.ImproperlyConfigured( raise exceptions.ImproperlyConfigured(
"Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or " "Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
" both GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET " "both GOOGLE_OAUTH2_CLIENT_ID and "
"in settings.py") "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): class OAuth2Settings(object):
@ -215,11 +322,11 @@ class OAuth2Settings(object):
Attributes: Attributes:
scopes: A list of OAuth2 scopes that the decorators and views will use scopes: A list of OAuth2 scopes that the decorators and views will use
as defaults as defaults.
request_prefix: The name of the attribute that the decorators use to request_prefix: The name of the attribute that the decorators use to
attach the UserOAuth2 object to the Django request object. attach the UserOAuth2 object to the Django request object.
client_id: The OAuth2 Client ID client_id: The OAuth2 Client ID.
client_secret: The OAuth2 Client Secret client_secret: The OAuth2 Client Secret.
""" """
def __init__(self, settings_instance): def __init__(self, settings_instance):
@ -232,75 +339,139 @@ class OAuth2Settings(object):
_get_oauth2_client_id_and_secret(settings_instance) _get_oauth2_client_id_and_secret(settings_instance)
if ('django.contrib.sessions.middleware.SessionMiddleware' if ('django.contrib.sessions.middleware.SessionMiddleware'
not in settings_instance.MIDDLEWARE_CLASSES): not in settings_instance.MIDDLEWARE_CLASSES):
raise exceptions.ImproperlyConfigured( raise exceptions.ImproperlyConfigured(
"The Google OAuth2 Helper requires session middleware to " 'The Google OAuth2 Helper requires session middleware to '
"be installed. Edit your MIDDLEWARE_CLASSES setting" 'be installed. Edit your MIDDLEWARE_CLASSES setting'
" to include 'django.contrib.sessions.middleware." ' to include \'django.contrib.sessions.middleware.'
"SessionMiddleware'.") 'SessionMiddleware\'.')
(self.storage_model, self.storage_model_user_property,
self.storage_model_credentials_property) = _get_storage_model()
oauth2_settings = OAuth2Settings(django.conf.settings) 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): def _redirect_with_params(url_name, *args, **kwargs):
"""Helper method to create a redirect response that uses GET URL """Helper method to create a redirect response with URL params.
parameters."""
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) url = urlresolvers.reverse(url_name, args=args)
params = parse.urlencode(kwargs, True) params = parse.urlencode(kwargs, True)
return "{0}?{1}".format(url, params) 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 UserOAuth2(object):
"""Class to create oauth2 objects on Django request objects containing """Class to create oauth2 objects on Django request objects containing
credentials and helper methods. credentials and helper methods.
""" """
def __init__(self, request, scopes=None, return_url=None): def __init__(self, request, scopes=None, return_url=None):
"""Initialize the Oauth2 Object """Initialize the Oauth2 Object.
:param request: Django request object
:param scopes: Scopes desired for this OAuth2 flow Args:
:param return_url: URL to return to after authorization is complete request: Django request object.
:return: 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.request = request
self.return_url = return_url or request.get_full_path() self.return_url = return_url or request.get_full_path()
self.scopes = set(oauth2_settings.scopes)
if scopes: if scopes:
self.scopes |= set(scopes) self._scopes = set(oauth2_settings.scopes) | set(scopes)
else:
# make sure previously requested custom scopes are maintained self._scopes = set(oauth2_settings.scopes)
# in future authorizations
credentials = storage.get_storage(self.request).get()
if credentials:
self.scopes |= credentials.scopes
def get_authorize_redirect(self): def get_authorize_redirect(self):
"""Creates a URl to start the OAuth2 authorization flow""" """Creates a URl to start the OAuth2 authorization flow."""
get_params = { get_params = {
'return_url': self.return_url, 'return_url': self.return_url,
'scopes': self.scopes 'scopes': self._get_scopes()
} }
return _redirect_with_params('google_oauth:authorize', return _redirect_with_params('google_oauth:authorize', **get_params)
**get_params)
def has_credentials(self): def has_credentials(self):
"""Returns True if there are valid credentials for the current user """Returns True if there are valid credentials for the current user
and required scopes.""" and required scopes."""
return (self.credentials and not self.credentials.invalid credentials = _credentials_from_request(self.request)
and self.credentials.has_scopes(self.scopes)) 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 @property
def credentials(self): def credentials(self):
"""Gets the authorized credentials for this flow, if they exist""" """Gets the authorized credentials for this flow, if they exist."""
return storage.get_storage(self.request).get() return _credentials_from_request(self.request)
@property @property
def http(self): def http(self):
"""Helper method to create an HTTP client authorized with OAuth2 """Helper method to create an HTTP client authorized with OAuth2
credentials""" credentials."""
if self.has_credentials(): if self.has_credentials():
return self.credentials.authorize(httplib2.Http()) return self.credentials.authorize(httplib2.Http())
return None return None

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Application Config For Django OAuth2 Helper """Application Config For Django OAuth2 Helper.
Django 1.7+ provides an Django 1.7+ provides an
[applications](https://docs.djangoproject.com/en/1.8/ref/applications/) [applications](https://docs.djangoproject.com/en/1.8/ref/applications/)

View File

@ -12,13 +12,29 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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 from django import shortcuts
from oauth2client.contrib import django_util import django.conf
from six import wraps 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): def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
""" Decorator to require OAuth2 credentials for a view """ Decorator to require OAuth2 credentials for a view.
.. code-block:: python .. code-block:: python
@ -36,21 +52,31 @@ def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
developerKey=API_KEY) developerKey=API_KEY)
events = service.events().list( events = service.events().list(
calendarId='primary').execute()['items'] calendarId='primary').execute()['items']
return HttpResponse("email: %s , calendar: %s" % (email, str(events))) return HttpResponse(
"email: {0}, calendar: {1}".format(email, str(events)))
:param decorated_function: View function to decorate, must have the Django Args:
request object as the first argument decorated_function: View function to decorate, must have the Django
:param scopes: Scopes to require, will default request object as the first argument.
:param decorator_kwargs: Can include ``return_url`` to specify the URL to scopes: Scopes to require, will default.
return to after OAuth2 authorization is complete decorator_kwargs: Can include ``return_url`` to specify the URL to
:return: An OAuth2 Authorize view if credentials are not found or if the return to after OAuth2 authorization is complete.
credentials are missing the required scopes. Otherwise,
the decorated view. 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): def curry_wrapper(wrapped_function):
@wraps(wrapped_function) @wraps(wrapped_function)
def required_wrapper(request, *args, **kwargs): 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', return_url = decorator_kwargs.pop('return_url',
request.get_full_path()) request.get_full_path())
user_oauth = django_util.UserOAuth2(request, scopes, return_url) user_oauth = django_util.UserOAuth2(request, scopes, return_url)
@ -84,21 +110,23 @@ def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
if request.oauth.has_credentials(): if request.oauth.has_credentials():
# this could be passed into a view # this could be passed into a view
# request.oauth.http is also initialized # request.oauth.http is also initialized
return HttpResponse("User email: %s" % return HttpResponse("User email: {0}".format(
request.oauth.credentials.id_token['email']) request.oauth.credentials.id_token['email'])
else: else:
return HttpResponse('Here is an OAuth Authorize link: return HttpResponse('Here is an OAuth Authorize link:
<a href="%s">Authorize</a>' % <a href="{0}">Authorize</a>'.format(
request.oauth.get_authorize_redirect()) request.oauth.get_authorize_redirect()))
:param decorated_function: View function to decorate Args:
:param scopes: Scopes to require, will default decorated_function: View function to decorate.
:param decorator_kwargs: Can include ``return_url`` to specify the URL to scopes: Scopes to require, will default.
return to after OAuth2 authorization is complete decorator_kwargs: Can include ``return_url`` to specify the URL to
:return: The decorated view function return to after OAuth2 authorization is complete.
Returns:
The decorated view function.
""" """
def curry_wrapper(wrapped_function): def curry_wrapper(wrapped_function):
@wraps(wrapped_function) @wraps(wrapped_function)
def enabled_wrapper(request, *args, **kwargs): def enabled_wrapper(request, *args, **kwargs):

View File

@ -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)

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
""" Signals for Google OAuth2 Helper """Signals for Google OAuth2 Helper.
This module contains signals for Google OAuth2 Helper. Currently it only This module contains signals for Google OAuth2 Helper. Currently it only
contains one, which fires when an OAuth2 authorization flow has completed. contains one, which fires when an OAuth2 authorization flow has completed.

View File

@ -12,7 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Contains Django URL patterns used for OAuth2 flow."""
from django.conf import urls from django.conf import urls
from oauth2client.contrib.django_util import views from oauth2client.contrib.django_util import views
urlpatterns = [ urlpatterns = [

View File

@ -12,16 +12,70 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from oauth2client.contrib.dictionary_storage import DictionaryStorage """Contains a storage module that stores credentials using the Django ORM."""
_CREDENTIALS_KEY = 'google_oauth2_credentials' from oauth2client import client
def get_storage(request): class DjangoORMStorage(client.Storage):
# TODO(issue 319): Make this pluggable with different storage providers """Store and retrieve a single credential to and from the Django datastore.
# https://github.com/google/oauth2client/issues/319
""" Gets a Credentials storage object for the Django OAuth2 Helper object This Storage helper presumes the Credentials
:param request: Reference to the current request object have been stored as a CredentialsField
:return: A OAuth2Client Storage implementation based on sessions on a db model class.
""" """
return DictionaryStorage(request.session, key=_CREDENTIALS_KEY)
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()

View File

@ -12,24 +12,46 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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 hashlib
import json import json
import os import os
import pickle import pickle
from django import http from django import http
from django.core import urlresolvers
from django import shortcuts 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 import client
from oauth2client.contrib import django_util from oauth2client.contrib import django_util
from oauth2client.contrib.django_util import get_storage
from oauth2client.contrib.django_util import signals from oauth2client.contrib.django_util import signals
from oauth2client.contrib.django_util import storage
_CSRF_KEY = 'google_oauth2_csrf_token' _CSRF_KEY = 'google_oauth2_csrf_token'
_FLOW_KEY = 'google_oauth2_flow_{0}' _FLOW_KEY = 'google_oauth2_flow_{0}'
def _make_flow(request, scopes, return_url=None): def _make_flow(request, scopes, return_url=None):
"""Creates a Web Server Flow""" """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. # Generate a CSRF token to prevent malicious requests.
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest() csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
@ -55,7 +77,17 @@ def _make_flow(request, scopes, return_url=None):
def _get_flow_for_token(csrf_token, request): def _get_flow_for_token(csrf_token, request):
""" Looks up the flow in session to recover information about requested """ Looks up the flow in session to recover information about requested
scopes.""" 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) flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
return None if flow_pickle is None else pickle.loads(flow_pickle) return None if flow_pickle is None else pickle.loads(flow_pickle)
@ -68,26 +100,30 @@ def oauth2_callback(request):
and redirects to the return_url specified in the authorize view and and redirects to the return_url specified in the authorize view and
stored in the session. stored in the session.
:param request: Django request Args:
:return: A redirect response back to the return_url request: Django request.
Returns:
A redirect response back to the return_url.
""" """
if 'error' in request.GET: if 'error' in request.GET:
reason = request.GET.get( reason = request.GET.get(
'error_description', request.GET.get('error', '')) 'error_description', request.GET.get('error', ''))
return http.HttpResponseBadRequest( return http.HttpResponseBadRequest(
'Authorization failed %s' % reason) 'Authorization failed {0}'.format(reason))
try: try:
encoded_state = request.GET['state'] encoded_state = request.GET['state']
code = request.GET['code'] code = request.GET['code']
except KeyError: except KeyError:
return http.HttpResponseBadRequest( return http.HttpResponseBadRequest(
"Request missing state or authorization code") 'Request missing state or authorization code')
try: try:
server_csrf = request.session[_CSRF_KEY] server_csrf = request.session[_CSRF_KEY]
except KeyError: except KeyError:
return http.HttpResponseBadRequest("No existing session for this flow.") return http.HttpResponseBadRequest(
'No existing session for this flow.')
try: try:
state = json.loads(encoded_state) state = json.loads(encoded_state)
@ -102,23 +138,24 @@ def oauth2_callback(request):
flow = _get_flow_for_token(client_csrf, request) flow = _get_flow_for_token(client_csrf, request)
if not flow: if not flow:
return http.HttpResponseBadRequest("Missing Oauth2 flow.") return http.HttpResponseBadRequest('Missing Oauth2 flow.')
try: try:
credentials = flow.step2_exchange(code) credentials = flow.step2_exchange(code)
except client.FlowExchangeError as exchange_error: except client.FlowExchangeError as exchange_error:
return http.HttpResponseBadRequest( return http.HttpResponseBadRequest(
"An error has occurred: {0}".format(exchange_error)) 'An error has occurred: {0}'.format(exchange_error))
storage.get_storage(request).put(credentials) get_storage(request).put(credentials)
signals.oauth2_authorized.send(sender=signals.oauth2_authorized, signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
request=request, credentials=credentials) request=request, credentials=credentials)
return shortcuts.redirect(return_url) return shortcuts.redirect(return_url)
def oauth2_authorize(request): def oauth2_authorize(request):
""" View to start the OAuth2 Authorization flow """ View to start the OAuth2 Authorization flow.
This view starts the OAuth2 authorization flow. If scopes is passed in This view starts the OAuth2 authorization flow. If scopes is passed in
as a GET URL parameter, it will authorize those scopes, otherwise the as a GET URL parameter, it will authorize those scopes, otherwise the
@ -126,12 +163,26 @@ def oauth2_authorize(request):
specified as a GET parameter, otherwise the referer header will 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. checked, and if that isn't found it will return to the root path.
:param request: The Django request object Args:
:return: A redirect to Google OAuth2 Authorization request: The Django request object.
Returns:
A redirect to Google OAuth2 Authorization.
""" """
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
return_url = request.GET.get('return_url', None) 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: if not return_url:
return_url = request.META.get('HTTP_REFERER', '/') return_url = request.META.get('HTTP_REFERER', '/')
flow = _make_flow(request=request, scopes=scopes, return_url=return_url) flow = _make_flow(request=request, scopes=scopes, return_url=return_url)

View File

@ -35,7 +35,7 @@ apiui/credential>`__.
app.config['SECRET_KEY'] = 'your-secret-key' app.config['SECRET_KEY'] = 'your-secret-key'
app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json' app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json'
# or, specify the client id and secret separately # or, specify the client id and secret separately
app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id' app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
@ -162,14 +162,11 @@ available outside of a request context, you will need to implement your own
:class:`oauth2client.Storage`. :class:`oauth2client.Storage`.
""" """
from functools import wraps
import hashlib import hashlib
import json import json
import os import os
import pickle import pickle
from functools import wraps
import six.moves.http_client as httplib
import httplib2
try: try:
from flask import Blueprint from flask import Blueprint
@ -182,10 +179,12 @@ try:
except ImportError: # pragma: NO COVER except ImportError: # pragma: NO COVER
raise ImportError('The flask utilities require flask 0.9 or newer.') raise ImportError('The flask utilities require flask 0.9 or newer.')
from oauth2client.client import FlowExchangeError import httplib2
from oauth2client.client import OAuth2WebServerFlow import six.moves.http_client as httplib
from oauth2client.contrib.dictionary_storage import DictionaryStorage
from oauth2client import client
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client.contrib import dictionary_storage
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)' __author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
@ -199,7 +198,7 @@ _CSRF_KEY = 'google_oauth2_csrf_token'
def _get_flow_for_token(csrf_token): def _get_flow_for_token(csrf_token):
"""Retrieves the flow instance associated with a given CSRF token from """Retrieves the flow instance associated with a given CSRF token from
the Flask session.""" the Flask session."""
flow_pickle = session.get( flow_pickle = session.pop(
_FLOW_KEY.format(csrf_token), None) _FLOW_KEY.format(csrf_token), None)
if flow_pickle is None: if flow_pickle is None:
@ -213,14 +212,14 @@ class UserOAuth2(object):
Configuration values: Configuration values:
* ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` path to a client secrets json * ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json
file, obtained from the credentials screen in the Google Developers file, obtained from the credentials screen in the Google Developers
console. console.
* ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` is not is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not
specified. specified.
* ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE``
is not specified. is not specified.
If app is specified, all arguments will be passed along to init_app. If app is specified, all arguments will be passed along to init_app.
@ -243,7 +242,7 @@ class UserOAuth2(object):
app: A Flask application. app: A Flask application.
scopes: Optional list of scopes to authorize. scopes: Optional list of scopes to authorize.
client_secrets_file: Path to a file containing client secrets. You client_secrets_file: Path to a file containing client secrets. You
can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_JSON config can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config
value. value.
client_id: If not specifying a client secrets file, specify the client_id: If not specifying a client secrets file, specify the
OAuth2 client id. You can also specify the OAuth2 client id. You can also specify the
@ -263,7 +262,8 @@ class UserOAuth2(object):
self.flow_kwargs = kwargs self.flow_kwargs = kwargs
if storage is None: if storage is None:
storage = DictionaryStorage(session, key=_CREDENTIALS_KEY) storage = dictionary_storage.DictionaryStorage(
session, key=_CREDENTIALS_KEY)
self.storage = storage self.storage = storage
if scopes is None: if scopes is None:
@ -307,8 +307,8 @@ class UserOAuth2(object):
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'OAuth2 configuration could not be found. Either specify the ' 'OAuth2 configuration could not be found. Either specify the '
'client_secrets_file or client_id and client_secret or set the' 'client_secrets_file or client_id and client_secret or set '
'app configuration variables ' 'the app configuration variables '
'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or ' 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.') 'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
@ -341,7 +341,7 @@ class UserOAuth2(object):
extra_scopes = kw.pop('scopes', []) extra_scopes = kw.pop('scopes', [])
scopes = set(self.scopes).union(set(extra_scopes)) scopes = set(self.scopes).union(set(extra_scopes))
flow = OAuth2WebServerFlow( flow = client.OAuth2WebServerFlow(
client_id=self.client_id, client_id=self.client_id,
client_secret=self.client_secret, client_secret=self.client_secret,
scope=scopes, scope=scopes,
@ -418,7 +418,7 @@ class UserOAuth2(object):
# Exchange the auth code for credentials. # Exchange the auth code for credentials.
try: try:
credentials = flow.step2_exchange(code) credentials = flow.step2_exchange(code)
except FlowExchangeError as exchange_error: except client.FlowExchangeError as exchange_error:
current_app.logger.exception(exchange_error) current_app.logger.exception(exchange_error)
content = 'An error occurred: {0}'.format(exchange_error) content = 'An error occurred: {0}'.format(exchange_error)
return content, httplib.BAD_REQUEST return content, httplib.BAD_REQUEST
@ -443,7 +443,14 @@ class UserOAuth2(object):
def has_credentials(self): def has_credentials(self):
"""Returns True if there are valid credentials for the current user.""" """Returns True if there are valid credentials for the current user."""
return self.credentials and not self.credentials.invalid 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 @property
def email(self): def email(self):

View File

@ -17,29 +17,19 @@
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
""" """
import json
import logging import logging
import warnings import warnings
import httplib2 import httplib2
from six.moves import http_client
from six.moves import urllib
from oauth2client._helpers import _from_bytes from oauth2client import client
from oauth2client import util from oauth2client.contrib import _metadata
from oauth2client.client import HttpAccessTokenRefreshError
from oauth2client.client import AssertionCredentials
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# URI Template for the endpoint that returns access_tokens.
_METADATA_ROOT = ('http://metadata.google.internal/computeMetadata/v1/'
'instance/service-accounts/default/')
META = _METADATA_ROOT + 'token'
_DEFAULT_EMAIL_METADATA = _METADATA_ROOT + 'email'
_SCOPES_WARNING = """\ _SCOPES_WARNING = """\
You have requested explicit scopes to be used with a GCE service account. 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 Using this argument will have no effect on the actual scopes for tokens
@ -48,31 +38,7 @@ can't be overridden in the request.
""" """
def _get_service_account_email(http_request=None): class AppAssertionCredentials(client.AssertionCredentials):
"""Get the GCE service account email from the current environment.
Args:
http_request: callable, (Optional) a callable that matches the method
signature of httplib2.Http.request, used to make
the request to the metadata service.
Returns:
tuple, A pair where the first entry is an optional response (from a
failed request) and the second is service account email found (as
a string).
"""
if http_request is None:
http_request = httplib2.Http().request
response, content = http_request(
_DEFAULT_EMAIL_METADATA, headers={'Metadata-Flavor': 'Google'})
if response.status == http_client.OK:
content = _from_bytes(content)
return None, content
else:
return response, content
class AppAssertionCredentials(AssertionCredentials):
"""Credentials object for Compute Engine Assertion Grants """Credentials object for Compute Engine Assertion Grants
This object will allow a Compute Engine instance to identify itself to This object will allow a Compute Engine instance to identify itself to
@ -83,34 +49,73 @@ class AppAssertionCredentials(AssertionCredentials):
This credential does not require a flow to instantiate because it This credential does not require a flow to instantiate because it
represents a two legged flow, and therefore has all of the required represents a two legged flow, and therefore has all of the required
information to generate and refresh its own access tokens. 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`.
""" """
@util.positional(2) def __init__(self, email=None, *args, **kwargs):
def __init__(self, scope='', **kwargs):
"""Constructor for AppAssertionCredentials """Constructor for AppAssertionCredentials
Args: Args:
scope: string or iterable of strings, scope(s) of the credentials email: an email that specifies the service account to use.
being requested. Using this argument will have no effect on Only necessary if using custom service accounts
the actual scopes for tokens requested. These scopes are (see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount).
set at VM instance creation time and won't change.
""" """
if scope: if 'scopes' in kwargs:
warnings.warn(_SCOPES_WARNING) warnings.warn(_SCOPES_WARNING)
# This is just provided for backwards compatibility, but is not kwargs['scopes'] = None
# used by this class.
self.scope = util.scopes_to_string(scope)
self.kwargs = kwargs
# Assertion type is no longer used, but still in the # Assertion type is no longer used, but still in the
# parent class signature. # parent class signature.
super(AppAssertionCredentials, self).__init__(None) super(AppAssertionCredentials, self).__init__(None, *args, **kwargs)
self._service_account_email = None
self.service_account_email = email
self.scopes = None
self.invalid = True
@classmethod @classmethod
def from_json(cls, json_data): def from_json(cls, json_data):
data = json.loads(_from_bytes(json_data)) raise NotImplementedError(
return AppAssertionCredentials(data['scope']) '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): def _refresh(self, http_request):
"""Refreshes the access_token. """Refreshes the access_token.
@ -125,21 +130,12 @@ class AppAssertionCredentials(AssertionCredentials):
Raises: Raises:
HttpAccessTokenRefreshError: When the refresh fails. HttpAccessTokenRefreshError: When the refresh fails.
""" """
response, content = http_request( try:
META, headers={'Metadata-Flavor': 'Google'}) self._retrieve_info(http_request)
content = _from_bytes(content) self.access_token, self.token_expiry = _metadata.get_token(
if response.status == http_client.OK: http_request, service_account=self.service_account_email)
try: except httplib2.HttpLib2Error as e:
token_content = json.loads(content) raise client.HttpAccessTokenRefreshError(str(e))
except Exception as e:
raise HttpAccessTokenRefreshError(str(e),
status=response.status)
self.access_token = token_content['access_token']
else:
if response.status == http_client.NOT_FOUND:
content += (' This can occur if a VM was created'
' with no service account or scopes.')
raise HttpAccessTokenRefreshError(content, status=response.status)
@property @property
def serialization_data(self): def serialization_data(self):
@ -149,9 +145,6 @@ class AppAssertionCredentials(AssertionCredentials):
def create_scoped_required(self): def create_scoped_required(self):
return False return False
def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self.kwargs)
def sign_blob(self, blob): def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes). """Cryptographically sign a blob (of bytes).
@ -167,28 +160,3 @@ class AppAssertionCredentials(AssertionCredentials):
""" """
raise NotImplementedError( raise NotImplementedError(
'Compute Engine service accounts cannot sign blobs') 'Compute Engine service accounts cannot sign blobs')
@property
def service_account_email(self):
"""Get the email for the current service account.
Uses the Google Compute Engine metadata service to retrieve the email
of the default service account.
Returns:
string, The email associated with the Google Compute Engine
service account.
Raises:
AttributeError, if the email can not be retrieved from the Google
Compute Engine metadata service.
"""
if self._service_account_email is None:
failure, email = _get_service_account_email()
if failure is None:
self._service_account_email = email
else:
raise AttributeError('Failed to retrieve the email from the '
'Google Compute Engine metadata service',
failure, email)
return self._service_account_email

View File

@ -21,14 +21,13 @@ import threading
import keyring import keyring
from oauth2client.client import Credentials from oauth2client import client
from oauth2client.client import Storage as BaseStorage
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
class Storage(BaseStorage): class Storage(client.Storage):
"""Store and retrieve a single credential to and from the keyring. """Store and retrieve a single credential to and from the keyring.
To use this module you must have the keyring module installed. See To use this module you must have the keyring module installed. See
@ -44,9 +43,9 @@ class Storage(BaseStorage):
Usage:: Usage::
from oauth2client.keyring_storage import Storage from oauth2client import keyring_storage
s = Storage('name_of_application', 'user1') s = keyring_storage.Storage('name_of_application', 'user1')
credentials = s.get() credentials = s.get()
""" """
@ -74,7 +73,7 @@ class Storage(BaseStorage):
if content is not None: if content is not None:
try: try:
credentials = Credentials.new_from_json(content) credentials = client.Credentials.new_from_json(content)
credentials.set_store(self) credentials.set_store(self)
except ValueError: except ValueError:
pass pass

View File

@ -57,7 +57,7 @@ class AlreadyLockedException(Exception):
def validate_file(filename): def validate_file(filename):
if os.path.islink(filename): if os.path.islink(filename):
raise CredentialsFileSymbolicLinkError( raise CredentialsFileSymbolicLinkError(
'File: %s is a symbolic link.' % filename) 'File: {0} is a symbolic link.'.format(filename))
class _Opener(object): class _Opener(object):
@ -122,8 +122,8 @@ class _PosixOpener(_Opener):
CredentialsFileSymbolicLinkError if the file is a symbolic link. CredentialsFileSymbolicLinkError if the file is a symbolic link.
""" """
if self._locked: if self._locked:
raise AlreadyLockedException('File %s is already locked' % raise AlreadyLockedException(
self._filename) 'File {0} is already locked'.format(self._filename))
self._locked = False self._locked = False
validate_file(self._filename) validate_file(self._filename)
@ -170,165 +170,7 @@ class _PosixOpener(_Opener):
def _posix_lockfile(self, filename): def _posix_lockfile(self, filename):
"""The name of the lock file to use for posix locking.""" """The name of the lock file to use for posix locking."""
return '%s.lock' % filename return '{0}.lock'.format(filename)
try:
import fcntl
class _FcntlOpener(_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 AlreadyLockedException('File %s is already locked' %
self._filename)
start_time = time.time()
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:
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()
except ImportError:
_FcntlOpener = None
try:
import pywintypes
import win32con
import win32file
class _Win32Opener(_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 AlreadyLockedException('File %s is already locked' %
self._filename)
start_time = time.time()
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:
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()
except ImportError:
_Win32Opener = None
class LockedFile(object): class LockedFile(object):
@ -347,10 +189,15 @@ class LockedFile(object):
""" """
opener = None opener = None
if not opener and use_native_locking: if not opener and use_native_locking:
if _Win32Opener: try:
from oauth2client.contrib._win32_opener import _Win32Opener
opener = _Win32Opener(filename, mode, fallback_mode) opener = _Win32Opener(filename, mode, fallback_mode)
if _FcntlOpener: except ImportError:
opener = _FcntlOpener(filename, mode, fallback_mode) try:
from oauth2client.contrib._fcntl_opener import _FcntlOpener
opener = _FcntlOpener(filename, mode, fallback_mode)
except ImportError:
pass
if not opener: if not opener:
opener = _PosixOpener(filename, mode, fallback_mode) opener = _PosixOpener(filename, mode, fallback_mode)

View File

@ -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)

View File

@ -50,16 +50,19 @@ import logging
import os import os
import threading import threading
from oauth2client.client import Credentials from oauth2client import client
from oauth2client.client import Storage as BaseStorage
from oauth2client import util from oauth2client import util
from oauth2client.contrib.locked_file import LockedFile from oauth2client.contrib import locked_file
__author__ = 'jbeda@google.com (Joe Beda)' __author__ = 'jbeda@google.com (Joe Beda)'
logger = logging.getLogger(__name__) 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 # A dict from 'filename'->_MultiStore instances
_multistores = {} _multistores = {}
_multistores_lock = threading.Lock() _multistores_lock = threading.Lock()
@ -108,7 +111,7 @@ def get_credential_storage(filename, client_id, user_agent, scope,
key = {'clientId': client_id, 'userAgent': user_agent, key = {'clientId': client_id, 'userAgent': user_agent,
'scope': util.scopes_to_string(scope)} 'scope': util.scopes_to_string(scope)}
return get_credential_storage_custom_key( return get_credential_storage_custom_key(
filename, key, warn_on_readonly=warn_on_readonly) filename, key, warn_on_readonly=warn_on_readonly)
@util.positional(2) @util.positional(2)
@ -131,7 +134,7 @@ def get_credential_storage_custom_string_key(filename, key_string,
# Create a key dictionary that can be used # Create a key dictionary that can be used
key_dict = {'key': key_string} key_dict = {'key': key_string}
return get_credential_storage_custom_key( return get_credential_storage_custom_key(
filename, key_dict, warn_on_readonly=warn_on_readonly) filename, key_dict, warn_on_readonly=warn_on_readonly)
@util.positional(2) @util.positional(2)
@ -209,7 +212,7 @@ class _MultiStore(object):
This will create the file if necessary. This will create the file if necessary.
""" """
self._file = LockedFile(filename, 'r+', 'r') self._file = locked_file.LockedFile(filename, 'r+', 'r')
self._thread_lock = threading.Lock() self._thread_lock = threading.Lock()
self._read_only = False self._read_only = False
self._warn_on_readonly = warn_on_readonly self._warn_on_readonly = warn_on_readonly
@ -225,7 +228,7 @@ class _MultiStore(object):
# If this is None, then the store hasn't been read yet. # If this is None, then the store hasn't been read yet.
self._data = None self._data = None
class _Storage(BaseStorage): class _Storage(client.Storage):
"""A Storage object that can read/write a single credential.""" """A Storage object that can read/write a single credential."""
def __init__(self, multistore, key): def __init__(self, multistore, key):
@ -298,7 +301,7 @@ class _MultiStore(object):
self._thread_lock.acquire() self._thread_lock.acquire()
try: try:
self._file.open_and_lock() self._file.open_and_lock()
except IOError as e: except (IOError, OSError) as e:
if e.errno == errno.ENOSYS: if e.errno == errno.ENOSYS:
logger.warn('File system does not support locking the ' logger.warn('File system does not support locking the '
'credentials file.') 'credentials file.')
@ -319,6 +322,7 @@ class _MultiStore(object):
'Opening in read-only mode. Any refreshed ' 'Opening in read-only mode. Any refreshed '
'credentials will only be ' 'credentials will only be '
'valid for this run.', self._file.filename()) 'valid for this run.', self._file.filename())
if os.path.getsize(self._file.filename()) == 0: if os.path.getsize(self._file.filename()) == 0:
logger.debug('Initializing empty multistore file') logger.debug('Initializing empty multistore file')
# The multistore is empty so write out an empty file. # The multistore is empty so write out an empty file.
@ -390,8 +394,8 @@ class _MultiStore(object):
'corrupt or an old version. Overwriting.') 'corrupt or an old version. Overwriting.')
if version > 1: if version > 1:
raise NewerCredentialStoreError( raise NewerCredentialStoreError(
'Credential file has file_version of %d. ' 'Credential file has file_version of {0}. '
'Only file_version of 1 is supported.' % version) 'Only file_version of 1 is supported.'.format(version))
credentials = [] credentials = []
try: try:
@ -421,7 +425,7 @@ class _MultiStore(object):
raw_key = cred_entry['key'] raw_key = cred_entry['key']
key = _dict_to_tuple_key(raw_key) key = _dict_to_tuple_key(raw_key)
credential = None credential = None
credential = Credentials.new_from_json( credential = client.Credentials.new_from_json(
json.dumps(cred_entry['credential'])) json.dumps(cred_entry['credential']))
return (key, credential) return (key, credential)

View File

@ -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()

View File

@ -19,7 +19,7 @@ import binascii
import hmac import hmac
import time import time
from oauth2client._helpers import _to_bytes from oauth2client import _helpers
from oauth2client import util from oauth2client import util
__authors__ = [ __authors__ = [
@ -49,12 +49,12 @@ def generate_token(key, user_id, action_id='', when=None):
Returns: Returns:
A string XSRF protection token. A string XSRF protection token.
""" """
digester = hmac.new(_to_bytes(key, encoding='utf-8')) digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8'))
digester.update(_to_bytes(str(user_id), encoding='utf-8')) digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8'))
digester.update(DELIMITER) digester.update(DELIMITER)
digester.update(_to_bytes(action_id, encoding='utf-8')) digester.update(_helpers._to_bytes(action_id, encoding='utf-8'))
digester.update(DELIMITER) digester.update(DELIMITER)
when = _to_bytes(str(when or int(time.time())), encoding='utf-8') when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8')
digester.update(when) digester.update(when)
digest = digester.digest() digest = digester.digest()

View File

@ -19,15 +19,13 @@ import json
import logging import logging
import time import time
from oauth2client._helpers import _from_bytes from oauth2client import _helpers
from oauth2client._helpers import _json_encode from oauth2client import _pure_python_crypt
from oauth2client._helpers import _to_bytes
from oauth2client._helpers import _urlsafe_b64decode
from oauth2client._helpers import _urlsafe_b64encode
from oauth2client._pure_python_crypt import RsaSigner
from oauth2client._pure_python_crypt import RsaVerifier
RsaSigner = _pure_python_crypt.RsaSigner
RsaVerifier = _pure_python_crypt.RsaVerifier
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
@ -44,17 +42,19 @@ def _bad_pkcs12_key_as_pem(*args, **kwargs):
try: try:
from oauth2client._openssl_crypt import OpenSSLVerifier from oauth2client import _openssl_crypt
from oauth2client._openssl_crypt import OpenSSLSigner OpenSSLSigner = _openssl_crypt.OpenSSLSigner
from oauth2client._openssl_crypt import pkcs12_key_as_pem OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
except ImportError: # pragma: NO COVER except ImportError: # pragma: NO COVER
OpenSSLVerifier = None OpenSSLVerifier = None
OpenSSLSigner = None OpenSSLSigner = None
pkcs12_key_as_pem = _bad_pkcs12_key_as_pem pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
try: try:
from oauth2client._pycrypto_crypt import PyCryptoVerifier from oauth2client import _pycrypto_crypt
from oauth2client._pycrypto_crypt import PyCryptoSigner PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
except ImportError: # pragma: NO COVER except ImportError: # pragma: NO COVER
PyCryptoVerifier = None PyCryptoVerifier = None
PyCryptoSigner = None PyCryptoSigner = None
@ -89,13 +89,13 @@ def make_signed_jwt(signer, payload, key_id=None):
header['kid'] = key_id header['kid'] = key_id
segments = [ segments = [
_urlsafe_b64encode(_json_encode(header)), _helpers._urlsafe_b64encode(_helpers._json_encode(header)),
_urlsafe_b64encode(_json_encode(payload)), _helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
] ]
signing_input = b'.'.join(segments) signing_input = b'.'.join(segments)
signature = signer.sign(signing_input) signature = signer.sign(signing_input)
segments.append(_urlsafe_b64encode(signature)) segments.append(_helpers._urlsafe_b64encode(signature))
logger.debug(str(segments)) logger.debug(str(segments))
@ -144,11 +144,11 @@ def _check_audience(payload_dict, audience):
audience_in_payload = payload_dict.get('aud') audience_in_payload = payload_dict.get('aud')
if audience_in_payload is None: if audience_in_payload is None:
raise AppIdentityError('No aud field in token: %s' % raise AppIdentityError(
(payload_dict,)) 'No aud field in token: {0}'.format(payload_dict))
if audience_in_payload != audience: if audience_in_payload != audience:
raise AppIdentityError('Wrong recipient, %s != %s: %s' % raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
(audience_in_payload, audience, payload_dict)) audience_in_payload, audience, payload_dict))
def _verify_time_range(payload_dict): def _verify_time_range(payload_dict):
@ -180,26 +180,28 @@ def _verify_time_range(payload_dict):
# Make sure issued at and expiration are in the payload. # Make sure issued at and expiration are in the payload.
issued_at = payload_dict.get('iat') issued_at = payload_dict.get('iat')
if issued_at is None: if issued_at is None:
raise AppIdentityError('No iat field in token: %s' % (payload_dict,)) raise AppIdentityError(
'No iat field in token: {0}'.format(payload_dict))
expiration = payload_dict.get('exp') expiration = payload_dict.get('exp')
if expiration is None: if expiration is None:
raise AppIdentityError('No exp field in token: %s' % (payload_dict,)) raise AppIdentityError(
'No exp field in token: {0}'.format(payload_dict))
# Make sure the expiration gives an acceptable token lifetime. # Make sure the expiration gives an acceptable token lifetime.
if expiration >= now + MAX_TOKEN_LIFETIME_SECS: if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
raise AppIdentityError('exp field too far in future: %s' % raise AppIdentityError(
(payload_dict,)) '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. # Make sure (up to clock skew) that the token wasn't issued in the future.
earliest = issued_at - CLOCK_SKEW_SECS earliest = issued_at - CLOCK_SKEW_SECS
if now < earliest: if now < earliest:
raise AppIdentityError('Token used too early, %d < %d: %s' % raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
(now, earliest, payload_dict)) now, earliest, payload_dict))
# Make sure (up to clock skew) that the token isn't already expired. # Make sure (up to clock skew) that the token isn't already expired.
latest = expiration + CLOCK_SKEW_SECS latest = expiration + CLOCK_SKEW_SECS
if now > latest: if now > latest:
raise AppIdentityError('Token used too late, %d > %d: %s' % raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
(now, latest, payload_dict)) now, latest, payload_dict))
def verify_signed_jwt_with_certs(jwt, certs, audience=None): def verify_signed_jwt_with_certs(jwt, certs, audience=None):
@ -219,22 +221,22 @@ def verify_signed_jwt_with_certs(jwt, certs, audience=None):
Raises: Raises:
AppIdentityError: if any checks are failed. AppIdentityError: if any checks are failed.
""" """
jwt = _to_bytes(jwt) jwt = _helpers._to_bytes(jwt)
if jwt.count(b'.') != 2: if jwt.count(b'.') != 2:
raise AppIdentityError( raise AppIdentityError(
'Wrong number of segments in token: %s' % (jwt,)) 'Wrong number of segments in token: {0}'.format(jwt))
header, payload, signature = jwt.split(b'.') header, payload, signature = jwt.split(b'.')
message_to_sign = header + b'.' + payload message_to_sign = header + b'.' + payload
signature = _urlsafe_b64decode(signature) signature = _helpers._urlsafe_b64decode(signature)
# Parse token. # Parse token.
payload_bytes = _urlsafe_b64decode(payload) payload_bytes = _helpers._urlsafe_b64decode(payload)
try: try:
payload_dict = json.loads(_from_bytes(payload_bytes)) payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
except: except:
raise AppIdentityError('Can\'t parse token: %s' % (payload_bytes,)) raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
# Verify that the signature matches the message. # Verify that the signature matches the message.
_verify_signature(message_to_sign, signature, certs.values()) _verify_signature(message_to_sign, signature, certs.values())

View File

@ -21,8 +21,7 @@ credentials.
import os import os
import threading import threading
from oauth2client.client import Credentials from oauth2client import client
from oauth2client.client import Storage as BaseStorage
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
@ -32,7 +31,7 @@ class CredentialsFileSymbolicLinkError(Exception):
"""Credentials files must not be symbolic links.""" """Credentials files must not be symbolic links."""
class Storage(BaseStorage): class Storage(client.Storage):
"""Store and retrieve a single credential to and from a file.""" """Store and retrieve a single credential to and from a file."""
def __init__(self, filename): def __init__(self, filename):
@ -42,7 +41,7 @@ class Storage(BaseStorage):
def _validate_file(self): def _validate_file(self):
if os.path.islink(self._filename): if os.path.islink(self._filename):
raise CredentialsFileSymbolicLinkError( raise CredentialsFileSymbolicLinkError(
'File: %s is a symbolic link.' % self._filename) 'File: {0} is a symbolic link.'.format(self._filename))
def locked_get(self): def locked_get(self):
"""Retrieve Credential from file. """Retrieve Credential from file.
@ -63,7 +62,7 @@ class Storage(BaseStorage):
return credentials return credentials
try: try:
credentials = Credentials.new_from_json(content) credentials = client.Credentials.new_from_json(content)
credentials.set_store(self) credentials.set_store(self)
except ValueError: except ValueError:
pass pass

View File

@ -20,16 +20,12 @@ import datetime
import json import json
import time import time
from oauth2client import GOOGLE_REVOKE_URI import oauth2client
from oauth2client import GOOGLE_TOKEN_URI from oauth2client import _helpers
from oauth2client._helpers import _json_encode from oauth2client import client
from oauth2client._helpers import _from_bytes
from oauth2client._helpers import _urlsafe_b64encode
from oauth2client import util
from oauth2client.client import AssertionCredentials
from oauth2client.client import EXPIRY_FORMAT
from oauth2client.client import SERVICE_ACCOUNT
from oauth2client import crypt from oauth2client import crypt
from oauth2client import transport
from oauth2client import util
_PASSWORD_DEFAULT = 'notasecret' _PASSWORD_DEFAULT = 'notasecret'
@ -44,7 +40,7 @@ to .pem format:
""" """
class ServiceAccountCredentials(AssertionCredentials): class ServiceAccountCredentials(client.AssertionCredentials):
"""Service Account credential for OAuth 2.0 signed JWT grants. """Service Account credential for OAuth 2.0 signed JWT grants.
Supports Supports
@ -73,6 +69,12 @@ class ServiceAccountCredentials(AssertionCredentials):
service account. service account.
user_agent: string, (Optional) User agent to use when sending user_agent: string, (Optional) User agent to use when sending
request. 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 kwargs: dict, Extra key-value pairs (both strings) to send in the
payload body when making an assertion. payload body when making an assertion.
""" """
@ -80,9 +82,9 @@ class ServiceAccountCredentials(AssertionCredentials):
MAX_TOKEN_LIFETIME_SECS = 3600 MAX_TOKEN_LIFETIME_SECS = 3600
"""Max lifetime of the token (one hour, in seconds).""" """Max lifetime of the token (one hour, in seconds)."""
NON_SERIALIZED_MEMBERS = ( NON_SERIALIZED_MEMBERS = (
frozenset(['_signer']) | frozenset(['_signer']) |
AssertionCredentials.NON_SERIALIZED_MEMBERS) client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
"""Members that aren't serialized when object is converted to JSON.""" """Members that aren't serialized when object is converted to JSON."""
# Can be over-ridden by factory constructors. Used for # Can be over-ridden by factory constructors. Used for
@ -98,10 +100,13 @@ class ServiceAccountCredentials(AssertionCredentials):
private_key_id=None, private_key_id=None,
client_id=None, client_id=None,
user_agent=None, user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
**kwargs): **kwargs):
super(ServiceAccountCredentials, self).__init__( super(ServiceAccountCredentials, self).__init__(
None, user_agent=user_agent) None, user_agent=user_agent, token_uri=token_uri,
revoke_uri=revoke_uri)
self._service_account_email = service_account_email self._service_account_email = service_account_email
self._signer = signer self._signer = signer
@ -121,8 +126,8 @@ class ServiceAccountCredentials(AssertionCredentials):
strip: array, An array of names of members to exclude from the strip: array, An array of names of members to exclude from the
JSON. JSON.
to_serialize: dict, (Optional) The properties for this object to_serialize: dict, (Optional) The properties for this object
that will be serialized. This allows callers to modify that will be serialized. This allows callers to
before serializing. modify before serializing.
Returns: Returns:
string, a JSON representation of this instance, suitable to pass to string, a JSON representation of this instance, suitable to pass to
@ -137,7 +142,8 @@ class ServiceAccountCredentials(AssertionCredentials):
strip, to_serialize=to_serialize) strip, to_serialize=to_serialize)
@classmethod @classmethod
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes): def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
token_uri=None, revoke_uri=None):
"""Helper for factory constructors from JSON keyfile. """Helper for factory constructors from JSON keyfile.
Args: Args:
@ -145,6 +151,12 @@ class ServiceAccountCredentials(AssertionCredentials):
containing the contents of the JSON keyfile. containing the contents of the JSON keyfile.
scopes: List or string, Scopes to use when acquiring an scopes: List or string, Scopes to use when acquiring an
access token. 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: Returns:
ServiceAccountCredentials, a credentials object created from ServiceAccountCredentials, a credentials object created from
@ -156,30 +168,45 @@ class ServiceAccountCredentials(AssertionCredentials):
the keyfile. the keyfile.
""" """
creds_type = keyfile_dict.get('type') creds_type = keyfile_dict.get('type')
if creds_type != SERVICE_ACCOUNT: if creds_type != client.SERVICE_ACCOUNT:
raise ValueError('Unexpected credentials type', creds_type, raise ValueError('Unexpected credentials type', creds_type,
'Expected', SERVICE_ACCOUNT) 'Expected', client.SERVICE_ACCOUNT)
service_account_email = keyfile_dict['client_email'] service_account_email = keyfile_dict['client_email']
private_key_pkcs8_pem = keyfile_dict['private_key'] private_key_pkcs8_pem = keyfile_dict['private_key']
private_key_id = keyfile_dict['private_key_id'] private_key_id = keyfile_dict['private_key_id']
client_id = keyfile_dict['client_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) signer = crypt.Signer.from_string(private_key_pkcs8_pem)
credentials = cls(service_account_email, signer, scopes=scopes, credentials = cls(service_account_email, signer, scopes=scopes,
private_key_id=private_key_id, private_key_id=private_key_id,
client_id=client_id) client_id=client_id, token_uri=token_uri,
revoke_uri=revoke_uri)
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
return credentials return credentials
@classmethod @classmethod
def from_json_keyfile_name(cls, filename, scopes=''): def from_json_keyfile_name(cls, filename, scopes='',
token_uri=None, revoke_uri=None):
"""Factory constructor from JSON keyfile by name. """Factory constructor from JSON keyfile by name.
Args: Args:
filename: string, The location of the keyfile. filename: string, The location of the keyfile.
scopes: List or string, (Optional) Scopes to use when acquiring an scopes: List or string, (Optional) Scopes to use when acquiring an
access token. 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: Returns:
ServiceAccountCredentials, a credentials object created from ServiceAccountCredentials, a credentials object created from
@ -192,10 +219,13 @@ class ServiceAccountCredentials(AssertionCredentials):
""" """
with open(filename, 'r') as file_obj: with open(filename, 'r') as file_obj:
client_credentials = json.load(file_obj) client_credentials = json.load(file_obj)
return cls._from_parsed_json_keyfile(client_credentials, scopes) return cls._from_parsed_json_keyfile(client_credentials, scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)
@classmethod @classmethod
def from_json_keyfile_dict(cls, keyfile_dict, scopes=''): def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
token_uri=None, revoke_uri=None):
"""Factory constructor from parsed JSON keyfile. """Factory constructor from parsed JSON keyfile.
Args: Args:
@ -203,6 +233,12 @@ class ServiceAccountCredentials(AssertionCredentials):
containing the contents of the JSON keyfile. containing the contents of the JSON keyfile.
scopes: List or string, (Optional) Scopes to use when acquiring an scopes: List or string, (Optional) Scopes to use when acquiring an
access token. 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: Returns:
ServiceAccountCredentials, a credentials object created from ServiceAccountCredentials, a credentials object created from
@ -213,12 +249,16 @@ class ServiceAccountCredentials(AssertionCredentials):
KeyError, if one of the expected keys is not present in KeyError, if one of the expected keys is not present in
the keyfile. the keyfile.
""" """
return cls._from_parsed_json_keyfile(keyfile_dict, scopes) return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)
@classmethod @classmethod
def _from_p12_keyfile_contents(cls, service_account_email, def _from_p12_keyfile_contents(cls, service_account_email,
private_key_pkcs12, private_key_pkcs12,
private_key_password=None, scopes=''): private_key_password=None, scopes='',
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile. """Factory constructor from JSON keyfile.
Args: Args:
@ -229,6 +269,12 @@ class ServiceAccountCredentials(AssertionCredentials):
private key. Defaults to ``notasecret``. private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an scopes: List or string, (Optional) Scopes to use when acquiring an
access token. 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: Returns:
ServiceAccountCredentials, a credentials object created from ServiceAccountCredentials, a credentials object created from
@ -244,14 +290,18 @@ class ServiceAccountCredentials(AssertionCredentials):
raise NotImplementedError(_PKCS12_ERROR) raise NotImplementedError(_PKCS12_ERROR)
signer = crypt.Signer.from_string(private_key_pkcs12, signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password) private_key_password)
credentials = cls(service_account_email, signer, scopes=scopes) 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_pkcs12 = private_key_pkcs12
credentials._private_key_password = private_key_password credentials._private_key_password = private_key_password
return credentials return credentials
@classmethod @classmethod
def from_p12_keyfile(cls, service_account_email, filename, def from_p12_keyfile(cls, service_account_email, filename,
private_key_password=None, scopes=''): private_key_password=None, scopes='',
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile. """Factory constructor from JSON keyfile.
Args: Args:
@ -262,6 +312,12 @@ class ServiceAccountCredentials(AssertionCredentials):
private key. Defaults to ``notasecret``. private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an scopes: List or string, (Optional) Scopes to use when acquiring an
access token. 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: Returns:
ServiceAccountCredentials, a credentials object created from ServiceAccountCredentials, a credentials object created from
@ -275,11 +331,14 @@ class ServiceAccountCredentials(AssertionCredentials):
private_key_pkcs12 = file_obj.read() private_key_pkcs12 = file_obj.read()
return cls._from_p12_keyfile_contents( return cls._from_p12_keyfile_contents(
service_account_email, private_key_pkcs12, service_account_email, private_key_pkcs12,
private_key_password=private_key_password, scopes=scopes) private_key_password=private_key_password, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)
@classmethod @classmethod
def from_p12_keyfile_buffer(cls, service_account_email, file_buffer, def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
private_key_password=None, scopes=''): private_key_password=None, scopes='',
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile. """Factory constructor from JSON keyfile.
Args: Args:
@ -291,6 +350,12 @@ class ServiceAccountCredentials(AssertionCredentials):
private key. Defaults to ``notasecret``. private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an scopes: List or string, (Optional) Scopes to use when acquiring an
access token. 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: Returns:
ServiceAccountCredentials, a credentials object created from ServiceAccountCredentials, a credentials object created from
@ -303,7 +368,8 @@ class ServiceAccountCredentials(AssertionCredentials):
private_key_pkcs12 = file_buffer.read() private_key_pkcs12 = file_buffer.read()
return cls._from_p12_keyfile_contents( return cls._from_p12_keyfile_contents(
service_account_email, private_key_pkcs12, service_account_email, private_key_pkcs12,
private_key_password=private_key_password, scopes=scopes) private_key_password=private_key_password, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)
def _generate_assertion(self): def _generate_assertion(self):
"""Generate the assertion that will be used in the request.""" """Generate the assertion that will be used in the request."""
@ -368,7 +434,7 @@ class ServiceAccountCredentials(AssertionCredentials):
ServiceAccountCredentials from the serialized data. ServiceAccountCredentials from the serialized data.
""" """
if not isinstance(json_data, dict): if not isinstance(json_data, dict):
json_data = json.loads(_from_bytes(json_data)) json_data = json.loads(_helpers._from_bytes(json_data))
private_key_pkcs8_pem = None private_key_pkcs8_pem = None
pkcs12_val = json_data.get(_PKCS12_KEY) pkcs12_val = json_data.get(_PKCS12_KEY)
@ -406,7 +472,7 @@ class ServiceAccountCredentials(AssertionCredentials):
token_expiry = json_data.get('token_expiry', None) token_expiry = json_data.get('token_expiry', None)
if token_expiry is not None: if token_expiry is not None:
credentials.token_expiry = datetime.datetime.strptime( credentials.token_expiry = datetime.datetime.strptime(
token_expiry, EXPIRY_FORMAT) token_expiry, client.EXPIRY_FORMAT)
return credentials return credentials
def create_scoped_required(self): def create_scoped_required(self):
@ -427,6 +493,33 @@ class ServiceAccountCredentials(AssertionCredentials):
result._private_key_password = self._private_key_password result._private_key_password = self._private_key_password
return result 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): def create_delegated(self, sub):
"""Create credentials that act as domain-wide delegation of authority. """Create credentials that act as domain-wide delegation of authority.
@ -446,18 +539,135 @@ class ServiceAccountCredentials(AssertionCredentials):
ServiceAccountCredentials, a copy of the current service account ServiceAccountCredentials, a copy of the current service account
updated to act on behalf of ``sub``. updated to act on behalf of ``sub``.
""" """
new_kwargs = dict(self._kwargs) return self.create_with_claims({'sub': sub})
new_kwargs['sub'] = sub
result = self.__class__(self._service_account_email,
self._signer, def _datetime_to_secs(utc_time):
scopes=self._scopes, # TODO(issue 298): use time_delta.total_seconds()
private_key_id=self._private_key_id, # time_delta.total_seconds() not supported in Python 2.6
client_id=self.client_id, epoch = datetime.datetime(1970, 1, 1)
user_agent=self._user_agent, time_delta = utc_time - epoch
**new_kwargs) return time_delta.days * 86400 + time_delta.seconds
result.token_uri = self.token_uri
result.revoke_uri = self.revoke_uri
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem class _JWTAccessCredentials(ServiceAccountCredentials):
result._private_key_pkcs12 = self._private_key_pkcs12 """Self signed JWT credentials.
result._private_key_password = self._private_key_password
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 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

View File

@ -27,8 +27,8 @@ import sys
from six.moves import BaseHTTPServer from six.moves import BaseHTTPServer
from six.moves import http_client from six.moves import http_client
from six.moves import urllib
from six.moves import input from six.moves import input
from six.moves import urllib
from oauth2client import client from oauth2client import client
from oauth2client import util from oauth2client import util
@ -42,17 +42,43 @@ _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
To make this sample run you will need to populate the client_secrets.json file To make this sample run you will need to populate the client_secrets.json file
found at: found at:
%s {file_path}
with information from the APIs Console <https://code.google.com/apis/console>. 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(): def _CreateArgumentParser():
try: try:
import argparse import argparse
except ImportError: except ImportError: # pragma: NO COVER
return None return None
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--auth_host_name', default='localhost', parser.add_argument('--auth_host_name', default='localhost',
@ -182,17 +208,11 @@ def run_flow(flow, storage, flags=None, http=None):
break break
flags.noauth_local_webserver = not success flags.noauth_local_webserver = not success
if not success: if not success:
print('Failed to start a local webserver listening ' print(_FAILED_START_MESSAGE)
'on either port 8080')
print('or port 8090. Please check your firewall settings and locally')
print('running programs that may be blocking or using those ports.')
print()
print('Falling back to --noauth_local_webserver and continuing with')
print('authorization.')
print()
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) oauth_callback = 'http://{host}:{port}/'.format(
host=flags.auth_host_name, port=port_number)
else: else:
oauth_callback = client.OOB_CALLBACK_URN oauth_callback = client.OOB_CALLBACK_URN
flow.redirect_uri = oauth_callback flow.redirect_uri = oauth_callback
@ -211,18 +231,9 @@ def run_flow(flow, storage, flags=None, http=None):
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
import webbrowser import webbrowser
webbrowser.open(authorize_url, new=1, autoraise=True) webbrowser.open(authorize_url, new=1, autoraise=True)
print('Your browser has been opened to visit:') print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url))
print()
print(' ' + authorize_url)
print()
print('If your browser is on a different machine then exit and re-run this')
print('after creating a file called nobrowser.txt in the same path as GAM.')
print()
else: else:
print('Go to the following link in your browser:') print(_GO_TO_LINK_MESSAGE.format(address=authorize_url))
print()
print(' ' + authorize_url)
print()
code = None code = None
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
@ -241,7 +252,7 @@ def run_flow(flow, storage, flags=None, http=None):
try: try:
credential = flow.step2_exchange(code, http=http) credential = flow.step2_exchange(code, http=http)
except client.FlowExchangeError as e: except client.FlowExchangeError as e:
sys.exit('Authentication has failed: %s' % e) sys.exit('Authentication has failed: {0}'.format(e))
storage.put(credential) storage.put(credential)
credential.set_store(storage) credential.set_store(storage)
@ -252,4 +263,4 @@ def run_flow(flow, storage, flags=None, http=None):
def message_if_missing(filename): def message_if_missing(filename):
"""Helpful message to display if the CLIENT_SECRETS file is missing.""" """Helpful message to display if the CLIENT_SECRETS file is missing."""
return _CLIENT_SECRETS_MESSAGE % filename return _CLIENT_SECRETS_MESSAGE.format(file_path=filename)

View File

@ -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())

View File

@ -124,16 +124,16 @@ def positional(max_positional_args):
plural_s = '' plural_s = ''
if max_positional_args != 1: if max_positional_args != 1:
plural_s = 's' plural_s = 's'
message = ('%s() takes at most %d positional ' message = ('{function}() takes at most {args_max} positional '
'argument%s (%d given)' % ( 'argument{plural} ({args_given} given)'.format(
wrapped.__name__, max_positional_args, function=wrapped.__name__,
plural_s, len(args))) args_max=max_positional_args,
args_given=len(args),
plural=plural_s))
if positional_parameters_enforcement == POSITIONAL_EXCEPTION: if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
raise TypeError(message) raise TypeError(message)
elif positional_parameters_enforcement == POSITIONAL_WARNING: elif positional_parameters_enforcement == POSITIONAL_WARNING:
logger.warning(message) logger.warning(message)
else: # IGNORE
pass
return wrapped(*args, **kwargs) return wrapped(*args, **kwargs)
return positional_wrapper return positional_wrapper