mirror of
https://github.com/GAM-team/GAM.git
synced 2025-07-03 19:23:44 +00:00
upgrade to oauth2client 3.0. Override some strings in GAM
to reduce custom changes to oauth2client.
This commit is contained in:
27
src/gam.py
27
src/gam.py
@ -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:
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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):
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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}
|
||||||
@ -1193,7 +1094,7 @@ class GoogleCredentials(OAuth2Credentials):
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@ -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))
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
81
src/oauth2client/contrib/_fcntl_opener.py
Normal file
81
src/oauth2client/contrib/_fcntl_opener.py
Normal 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()
|
123
src/oauth2client/contrib/_metadata.py
Normal file
123
src/oauth2client/contrib/_metadata.py
Normal 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
|
106
src/oauth2client/contrib/_win32_opener.py
Normal file
106
src/oauth2client/contrib/_win32_opener.py
Normal 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()
|
@ -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("'", ''')
|
return cgi.escape(s, quote=1).replace("'", ''')
|
||||||
|
|
||||||
|
|
||||||
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
|
return uri
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
@ -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)
|
||||||
@ -202,8 +282,35 @@ def _get_oauth2_client_id_and_secret(settings_instance):
|
|||||||
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):
|
||||||
@ -234,73 +341,137 @@ class OAuth2Settings(object):
|
|||||||
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
|
||||||
|
@ -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/)
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An OAuth2 Authorize view if credentials are not found or if the
|
||||||
credentials are missing the required scopes. Otherwise,
|
credentials are missing the required scopes. Otherwise,
|
||||||
the decorated view.
|
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):
|
||||||
|
75
src/oauth2client/contrib/django_util/models.py
Normal file
75
src/oauth2client/contrib/django_util/models.py
Normal 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)
|
@ -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.
|
||||||
|
@ -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 = [
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
|
||||||
META, headers={'Metadata-Flavor': 'Google'})
|
|
||||||
content = _from_bytes(content)
|
|
||||||
if response.status == http_client.OK:
|
|
||||||
try:
|
try:
|
||||||
token_content = json.loads(content)
|
self._retrieve_info(http_request)
|
||||||
except Exception as e:
|
self.access_token, self.token_expiry = _metadata.get_token(
|
||||||
raise HttpAccessTokenRefreshError(str(e),
|
http_request, service_account=self.service_account_email)
|
||||||
status=response.status)
|
except httplib2.HttpLib2Error as e:
|
||||||
self.access_token = token_content['access_token']
|
raise client.HttpAccessTokenRefreshError(str(e))
|
||||||
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
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
try:
|
||||||
|
from oauth2client.contrib._fcntl_opener import _FcntlOpener
|
||||||
opener = _FcntlOpener(filename, mode, fallback_mode)
|
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)
|
||||||
|
355
src/oauth2client/contrib/multiprocess_file_storage.py
Normal file
355
src/oauth2client/contrib/multiprocess_file_storage.py
Normal 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)
|
@ -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()
|
||||||
@ -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)
|
||||||
|
|
||||||
|
173
src/oauth2client/contrib/sqlalchemy.py
Normal file
173
src/oauth2client/contrib/sqlalchemy.py
Normal 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()
|
@ -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()
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
@ -82,7 +84,7 @@ class ServiceAccountCredentials(AssertionCredentials):
|
|||||||
|
|
||||||
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,
|
|
||||||
|
def _datetime_to_secs(utc_time):
|
||||||
|
# TODO(issue 298): use time_delta.total_seconds()
|
||||||
|
# time_delta.total_seconds() not supported in Python 2.6
|
||||||
|
epoch = datetime.datetime(1970, 1, 1)
|
||||||
|
time_delta = utc_time - epoch
|
||||||
|
return time_delta.days * 86400 + time_delta.seconds
|
||||||
|
|
||||||
|
|
||||||
|
class _JWTAccessCredentials(ServiceAccountCredentials):
|
||||||
|
"""Self signed JWT credentials.
|
||||||
|
|
||||||
|
Makes an assertion to server using a self signed JWT from service account
|
||||||
|
credentials. These credentials do NOT use OAuth 2.0 and instead
|
||||||
|
authenticate directly.
|
||||||
|
"""
|
||||||
|
_MAX_TOKEN_LIFETIME_SECS = 3600
|
||||||
|
"""Max lifetime of the token (one hour, in seconds)."""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
service_account_email,
|
||||||
|
signer,
|
||||||
|
scopes=None,
|
||||||
|
private_key_id=None,
|
||||||
|
client_id=None,
|
||||||
|
user_agent=None,
|
||||||
|
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||||
|
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||||
|
additional_claims=None):
|
||||||
|
if additional_claims is None:
|
||||||
|
additional_claims = {}
|
||||||
|
super(_JWTAccessCredentials, self).__init__(
|
||||||
|
service_account_email,
|
||||||
|
signer,
|
||||||
|
private_key_id=private_key_id,
|
||||||
|
client_id=client_id,
|
||||||
|
user_agent=user_agent,
|
||||||
|
token_uri=token_uri,
|
||||||
|
revoke_uri=revoke_uri,
|
||||||
|
**additional_claims)
|
||||||
|
|
||||||
|
def authorize(self, http):
|
||||||
|
"""Authorize an httplib2.Http instance with a JWT assertion.
|
||||||
|
|
||||||
|
Unless specified, the 'aud' of the assertion will be the base
|
||||||
|
uri of the request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
http: An instance of ``httplib2.Http`` or something that acts
|
||||||
|
like it.
|
||||||
|
Returns:
|
||||||
|
A modified instance of http that was passed in.
|
||||||
|
Example::
|
||||||
|
h = httplib2.Http()
|
||||||
|
h = credentials.authorize(h)
|
||||||
|
"""
|
||||||
|
transport.wrap_http_for_jwt_access(self, http)
|
||||||
|
return http
|
||||||
|
|
||||||
|
def get_access_token(self, http=None, additional_claims=None):
|
||||||
|
"""Create a signed jwt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
http: unused
|
||||||
|
additional_claims: dict, additional claims to add to
|
||||||
|
the payload of the JWT.
|
||||||
|
Returns:
|
||||||
|
An AccessTokenInfo with the signed jwt
|
||||||
|
"""
|
||||||
|
if additional_claims is None:
|
||||||
|
if self.access_token is None or self.access_token_expired:
|
||||||
|
self.refresh(None)
|
||||||
|
return client.AccessTokenInfo(
|
||||||
|
access_token=self.access_token, expires_in=self._expires_in())
|
||||||
|
else:
|
||||||
|
# Create a 1 time token
|
||||||
|
token, unused_expiry = self._create_token(additional_claims)
|
||||||
|
return client.AccessTokenInfo(
|
||||||
|
access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
|
||||||
|
|
||||||
|
def revoke(self, http):
|
||||||
|
"""Cannot revoke JWTAccessCredentials tokens."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_scoped_required(self):
|
||||||
|
# JWTAccessCredentials are unscoped by definition
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||||
|
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||||
|
# Returns an OAuth2 credentials with the given scope
|
||||||
|
result = ServiceAccountCredentials(self._service_account_email,
|
||||||
self._signer,
|
self._signer,
|
||||||
scopes=self._scopes,
|
scopes=scopes,
|
||||||
private_key_id=self._private_key_id,
|
private_key_id=self._private_key_id,
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
user_agent=self._user_agent,
|
user_agent=self._user_agent,
|
||||||
**new_kwargs)
|
token_uri=token_uri,
|
||||||
result.token_uri = self.token_uri
|
revoke_uri=revoke_uri,
|
||||||
result.revoke_uri = self.revoke_uri
|
**self._kwargs)
|
||||||
|
if self._private_key_pkcs8_pem is not None:
|
||||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
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
|
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||||
|
if self._private_key_password is not None:
|
||||||
result._private_key_password = self._private_key_password
|
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
|
||||||
|
@ -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)
|
||||||
|
245
src/oauth2client/transport.py
Normal file
245
src/oauth2client/transport.py
Normal 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())
|
@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user