Delete duplicated oauth2client library (#852)

This commit is contained in:
Ross Scroggs
2019-02-28 04:05:55 -08:00
committed by Jay Lee
parent 45027da057
commit cbfb0a7310
33 changed files with 0 additions and 8717 deletions

View File

@@ -1,24 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Client library for using OAuth2, especially with Google APIs."""
__version__ = '4.1.3'
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
GOOGLE_DEVICE_URI = 'https://oauth2.googleapis.com/device/code'
GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'
GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token'
GOOGLE_TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo'

View File

@@ -1,341 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper functions for commonly used utilities."""
import base64
import functools
import inspect
import json
import logging
import os
import warnings
import six
from six.moves import urllib
logger = logging.getLogger(__name__)
POSITIONAL_WARNING = 'WARNING'
POSITIONAL_EXCEPTION = 'EXCEPTION'
POSITIONAL_IGNORE = 'IGNORE'
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
POSITIONAL_IGNORE])
positional_parameters_enforcement = POSITIONAL_WARNING
_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
_IS_DIR_MESSAGE = '{0}: Is a directory'
_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
def positional(max_positional_args):
"""A decorator to declare that only the first N arguments my be positional.
This decorator makes it easy to support Python 3 style keyword-only
parameters. For example, in Python 3 it is possible to write::
def fn(pos1, *, kwonly1=None, kwonly1=None):
...
All named parameters after ``*`` must be a keyword::
fn(10, 'kw1', 'kw2') # Raises exception.
fn(10, kwonly1='kw1') # Ok.
Example
^^^^^^^
To define a function like above, do::
@positional(1)
def fn(pos1, kwonly1=None, kwonly2=None):
...
If no default value is provided to a keyword argument, it becomes a
required keyword argument::
@positional(0)
def fn(required_kw):
...
This must be called with the keyword parameter::
fn() # Raises exception.
fn(10) # Raises exception.
fn(required_kw=10) # Ok.
When defining instance or class methods always remember to account for
``self`` and ``cls``::
class MyClass(object):
@positional(2)
def my_method(self, pos1, kwonly1=None):
...
@classmethod
@positional(2)
def my_method(cls, pos1, kwonly1=None):
...
The positional decorator behavior is controlled by
``_helpers.positional_parameters_enforcement``, which may be set to
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
nothing, respectively, if a declaration is violated.
Args:
max_positional_arguments: Maximum number of positional arguments. All
parameters after the this index must be
keyword only.
Returns:
A decorator that prevents using arguments after max_positional_args
from being used as positional parameters.
Raises:
TypeError: if a key-word only argument is provided as a positional
parameter, but only if
_helpers.positional_parameters_enforcement is set to
POSITIONAL_EXCEPTION.
"""
def positional_decorator(wrapped):
@functools.wraps(wrapped)
def positional_wrapper(*args, **kwargs):
if len(args) > max_positional_args:
plural_s = ''
if max_positional_args != 1:
plural_s = 's'
message = ('{function}() takes at most {args_max} positional '
'argument{plural} ({args_given} given)'.format(
function=wrapped.__name__,
args_max=max_positional_args,
args_given=len(args),
plural=plural_s))
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
raise TypeError(message)
elif positional_parameters_enforcement == POSITIONAL_WARNING:
logger.warning(message)
return wrapped(*args, **kwargs)
return positional_wrapper
if isinstance(max_positional_args, six.integer_types):
return positional_decorator
else:
args, _, _, defaults = inspect.getargspec(max_positional_args)
return positional(len(args) - len(defaults))(max_positional_args)
def scopes_to_string(scopes):
"""Converts scope value to a string.
If scopes is a string then it is simply passed through. If scopes is an
iterable then a string is returned that is all the individual scopes
concatenated with spaces.
Args:
scopes: string or iterable of strings, the scopes.
Returns:
The scopes formatted as a single string.
"""
if isinstance(scopes, six.string_types):
return scopes
else:
return ' '.join(scopes)
def string_to_scopes(scopes):
"""Converts stringifed scope value to a list.
If scopes is a list then it is simply passed through. If scopes is an
string then a list of each individual scope is returned.
Args:
scopes: a string or iterable of strings, the scopes.
Returns:
The scopes in a list.
"""
if not scopes:
return []
elif isinstance(scopes, six.string_types):
return scopes.split(' ')
else:
return scopes
def parse_unique_urlencoded(content):
"""Parses unique key-value parameters from urlencoded content.
Args:
content: string, URL-encoded key-value pairs.
Returns:
dict, The key-value pairs from ``content``.
Raises:
ValueError: if one of the keys is repeated.
"""
urlencoded_params = urllib.parse.parse_qs(content)
params = {}
for key, value in six.iteritems(urlencoded_params):
if len(value) != 1:
msg = ('URL-encoded content contains a repeated value:'
'%s -> %s' % (key, ', '.join(value)))
raise ValueError(msg)
params[key] = value[0]
return params
def update_query_params(uri, params):
"""Updates a URI with new query parameters.
If a given key from ``params`` is repeated in the ``uri``, then
the URI will be considered invalid and an error will occur.
If the URI is valid, then each value from ``params`` will
replace the corresponding value in the query parameters (if
it exists).
Args:
uri: string, A valid URI, with potential existing query parameters.
params: dict, A dictionary of query parameters.
Returns:
The same URI but with the new query parameters added.
"""
parts = urllib.parse.urlparse(uri)
query_params = parse_unique_urlencoded(parts.query)
query_params.update(params)
new_query = urllib.parse.urlencode(query_params)
new_parts = parts._replace(query=new_query)
return urllib.parse.urlunparse(new_parts)
def _add_query_parameter(url, name, value):
"""Adds a query parameter to a url.
Replaces the current value if it already exists in the URL.
Args:
url: string, url to add the query parameter to.
name: string, query parameter name.
value: string, query parameter value.
Returns:
Updated query parameter. Does not update the url if value is None.
"""
if value is None:
return url
else:
return update_query_params(url, {name: value})
def validate_file(filename):
if os.path.islink(filename):
raise IOError(_SYM_LINK_MESSAGE.format(filename))
elif os.path.isdir(filename):
raise IOError(_IS_DIR_MESSAGE.format(filename))
elif not os.path.isfile(filename):
warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
def _parse_pem_key(raw_key_input):
"""Identify and extract PEM keys.
Determines whether the given key is in the format of PEM key, and extracts
the relevant part of the key if it is.
Args:
raw_key_input: The contents of a private key file (either PEM or
PKCS12).
Returns:
string, The actual key if the contents are from a PEM file, or
else None.
"""
offset = raw_key_input.find(b'-----BEGIN ')
if offset != -1:
return raw_key_input[offset:]
def _json_encode(data):
return json.dumps(data, separators=(',', ':'))
def _to_bytes(value, encoding='ascii'):
"""Converts a string value to bytes, if necessary.
Unfortunately, ``six.b`` is insufficient for this task since in
Python2 it does not modify ``unicode`` objects.
Args:
value: The string/bytes value to be converted.
encoding: The encoding to use to convert unicode to bytes. Defaults
to "ascii", which will not allow any characters from ordinals
larger than 127. Other useful values are "latin-1", which
which will only allows byte ordinals (up to 255) and "utf-8",
which will encode any unicode that needs to be.
Returns:
The original value converted to bytes (if unicode) or as passed in
if it started out as bytes.
Raises:
ValueError if the value could not be converted to bytes.
"""
result = (value.encode(encoding)
if isinstance(value, six.text_type) else value)
if isinstance(result, six.binary_type):
return result
else:
raise ValueError('{0!r} could not be converted to bytes'.format(value))
def _from_bytes(value):
"""Converts bytes to a string value, if necessary.
Args:
value: The string/bytes value to be converted.
Returns:
The original value converted to unicode (if bytes) or as passed in
if it started out as unicode.
Raises:
ValueError if the value could not be converted to unicode.
"""
result = (value.decode('utf-8')
if isinstance(value, six.binary_type) else value)
if isinstance(result, six.text_type):
return result
else:
raise ValueError(
'{0!r} could not be converted to unicode'.format(value))
def _urlsafe_b64encode(raw_bytes):
raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
def _urlsafe_b64decode(b64string):
# Guard against unicode strings, which base64 can't handle.
b64string = _to_bytes(b64string)
padded = b64string + b'=' * (4 - len(b64string) % 4)
return base64.urlsafe_b64decode(padded)

View File

@@ -1,136 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""OpenSSL Crypto-related routines for oauth2client."""
from OpenSSL import crypto
from oauth2client import _helpers
class OpenSSLVerifier(object):
"""Verifies the signature on a message."""
def __init__(self, pubkey):
"""Constructor.
Args:
pubkey: OpenSSL.crypto.PKey, The public key to verify with.
"""
self._pubkey = pubkey
def verify(self, message, signature):
"""Verifies a message against a signature.
Args:
message: string or bytes, The message to verify. If string, will be
encoded to bytes as utf-8.
signature: string or bytes, The signature on the message. If string,
will be encoded to bytes as utf-8.
Returns:
True if message was signed by the private key associated with the
public key that this object was constructed with.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
signature = _helpers._to_bytes(signature, encoding='utf-8')
try:
crypto.verify(self._pubkey, signature, message, 'sha256')
return True
except crypto.Error:
return False
@staticmethod
def from_string(key_pem, is_x509_cert):
"""Construct a Verified instance from a string.
Args:
key_pem: string, public key in PEM format.
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
is expected to be an RSA key in PEM format.
Returns:
Verifier instance.
Raises:
OpenSSL.crypto.Error: if the key_pem can't be parsed.
"""
key_pem = _helpers._to_bytes(key_pem)
if is_x509_cert:
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
else:
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
return OpenSSLVerifier(pubkey)
class OpenSSLSigner(object):
"""Signs messages with a private key."""
def __init__(self, pkey):
"""Constructor.
Args:
pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
"""
self._key = pkey
def sign(self, message):
"""Signs a message.
Args:
message: bytes, Message to be signed.
Returns:
string, The signature of the message for the given key.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
return crypto.sign(self._key, message, 'sha256')
@staticmethod
def from_string(key, password=b'notasecret'):
"""Construct a Signer instance from a string.
Args:
key: string, private key in PKCS12 or PEM format.
password: string, password for the private key file.
Returns:
Signer instance.
Raises:
OpenSSL.crypto.Error if the key can't be parsed.
"""
key = _helpers._to_bytes(key)
parsed_pem_key = _helpers._parse_pem_key(key)
if parsed_pem_key:
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
else:
password = _helpers._to_bytes(password, encoding='utf-8')
pkey = crypto.load_pkcs12(key, password).get_privatekey()
return OpenSSLSigner(pkey)
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
Args:
private_key_bytes: Bytes. PKCS#12 key in DER format.
private_key_password: String. Password for PKCS#12 key.
Returns:
String. PEM contents of ``private_key_bytes``.
"""
private_key_password = _helpers._to_bytes(private_key_password)
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
pkcs12.get_privatekey())

View File

@@ -1,67 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
Public Clients
See RFC7636.
"""
import base64
import hashlib
import os
def code_verifier(n_bytes=64):
"""
Generates a 'code_verifier' as described in section 4.1 of RFC 7636.
This is a 'high-entropy cryptographic random string' that will be
impractical for an attacker to guess.
Args:
n_bytes: integer between 31 and 96, inclusive. default: 64
number of bytes of entropy to include in verifier.
Returns:
Bytestring, representing urlsafe base64-encoded random data.
"""
verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=')
# https://tools.ietf.org/html/rfc7636#section-4.1
# minimum length of 43 characters and a maximum length of 128 characters.
if len(verifier) < 43:
raise ValueError("Verifier too short. n_bytes must be > 30.")
elif len(verifier) > 128:
raise ValueError("Verifier too long. n_bytes must be < 97.")
else:
return verifier
def code_challenge(verifier):
"""
Creates a 'code_challenge' as described in section 4.2 of RFC 7636
by taking the sha256 hash of the verifier and then urlsafe
base64-encoding it.
Args:
verifier: bytestring, representing a code_verifier as generated by
code_verifier().
Returns:
Bytestring, representing a urlsafe base64-encoded sha256 hash digest,
without '=' padding.
"""
digest = hashlib.sha256(verifier).digest()
return base64.urlsafe_b64encode(digest).rstrip(b'=')

View File

@@ -1,184 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pure Python crypto-related routines for oauth2client.
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
certificates.
"""
from pyasn1.codec.der import decoder
from pyasn1_modules import pem
from pyasn1_modules.rfc2459 import Certificate
from pyasn1_modules.rfc5208 import PrivateKeyInfo
import rsa
import six
from oauth2client import _helpers
_PKCS12_ERROR = r"""\
PKCS12 format is not supported by the RSA library.
Either install PyOpenSSL, or please convert .p12 format
to .pem format:
$ cat key.p12 | \
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
> openssl rsa > key.pem
"""
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
'-----END RSA PRIVATE KEY-----')
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
'-----END PRIVATE KEY-----')
_PKCS8_SPEC = PrivateKeyInfo()
def _bit_list_to_bytes(bit_list):
"""Converts an iterable of 1's and 0's to bytes.
Combines the list 8 at a time, treating each group of 8 bits
as a single byte.
"""
num_bits = len(bit_list)
byte_vals = bytearray()
for start in six.moves.xrange(0, num_bits, 8):
curr_bits = bit_list[start:start + 8]
char_val = sum(val * digit
for val, digit in zip(_POW2, curr_bits))
byte_vals.append(char_val)
return bytes(byte_vals)
class RsaVerifier(object):
"""Verifies the signature on a message.
Args:
pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
"""
def __init__(self, pubkey):
self._pubkey = pubkey
def verify(self, message, signature):
"""Verifies a message against a signature.
Args:
message: string or bytes, The message to verify. If string, will be
encoded to bytes as utf-8.
signature: string or bytes, The signature on the message. If
string, will be encoded to bytes as utf-8.
Returns:
True if message was signed by the private key associated with the
public key that this object was constructed with.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
try:
return rsa.pkcs1.verify(message, signature, self._pubkey)
except (ValueError, rsa.pkcs1.VerificationError):
return False
@classmethod
def from_string(cls, key_pem, is_x509_cert):
"""Construct an RsaVerifier instance from a string.
Args:
key_pem: string, public key in PEM format.
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
is expected to be an RSA key in PEM format.
Returns:
RsaVerifier instance.
Raises:
ValueError: if the key_pem can't be parsed. In either case, error
will begin with 'No PEM start marker'. If
``is_x509_cert`` is True, will fail to find the
"-----BEGIN CERTIFICATE-----" error, otherwise fails
to find "-----BEGIN RSA PUBLIC KEY-----".
"""
key_pem = _helpers._to_bytes(key_pem)
if is_x509_cert:
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
if remaining != b'':
raise ValueError('Unused bytes', remaining)
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
else:
pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
return cls(pubkey)
class RsaSigner(object):
"""Signs messages with a private key.
Args:
pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
"""
def __init__(self, pkey):
self._key = pkey
def sign(self, message):
"""Signs a message.
Args:
message: bytes, Message to be signed.
Returns:
string, The signature of the message for the given key.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
@classmethod
def from_string(cls, key, password='notasecret'):
"""Construct an RsaSigner instance from a string.
Args:
key: string, private key in PEM format.
password: string, password for private key file. Unused for PEM
files.
Returns:
RsaSigner instance.
Raises:
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
PEM format.
"""
key = _helpers._from_bytes(key) # pem expects str in Py3
marker_id, key_bytes = pem.readPemBlocksFromFile(
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
if marker_id == 0:
pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
format='DER')
elif marker_id == 1:
key_info, remaining = decoder.decode(
key_bytes, asn1Spec=_PKCS8_SPEC)
if remaining != b'':
raise ValueError('Unused bytes', remaining)
pkey_info = key_info.getComponentByName('privateKey')
pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
format='DER')
else:
raise ValueError('No key could be detected.')
return cls(pkey)

View File

@@ -1,124 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""pyCrypto Crypto-related routines for oauth2client."""
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Util.asn1 import DerSequence
from oauth2client import _helpers
class PyCryptoVerifier(object):
"""Verifies the signature on a message."""
def __init__(self, pubkey):
"""Constructor.
Args:
pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
with.
"""
self._pubkey = pubkey
def verify(self, message, signature):
"""Verifies a message against a signature.
Args:
message: string or bytes, The message to verify. If string, will be
encoded to bytes as utf-8.
signature: string or bytes, The signature on the message.
Returns:
True if message was signed by the private key associated with the
public key that this object was constructed with.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
return PKCS1_v1_5.new(self._pubkey).verify(
SHA256.new(message), signature)
@staticmethod
def from_string(key_pem, is_x509_cert):
"""Construct a Verified instance from a string.
Args:
key_pem: string, public key in PEM format.
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
is expected to be an RSA key in PEM format.
Returns:
Verifier instance.
"""
if is_x509_cert:
key_pem = _helpers._to_bytes(key_pem)
pemLines = key_pem.replace(b' ', b'').split()
certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
certSeq = DerSequence()
certSeq.decode(certDer)
tbsSeq = DerSequence()
tbsSeq.decode(certSeq[0])
pubkey = RSA.importKey(tbsSeq[6])
else:
pubkey = RSA.importKey(key_pem)
return PyCryptoVerifier(pubkey)
class PyCryptoSigner(object):
"""Signs messages with a private key."""
def __init__(self, pkey):
"""Constructor.
Args:
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
"""
self._key = pkey
def sign(self, message):
"""Signs a message.
Args:
message: string, Message to be signed.
Returns:
string, The signature of the message for the given key.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
@staticmethod
def from_string(key, password='notasecret'):
"""Construct a Signer instance from a string.
Args:
key: string, private key in PEM format.
password: string, password for private key file. Unused for PEM
files.
Returns:
Signer instance.
Raises:
NotImplementedError if the key isn't in PEM format.
"""
parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
if parsed_pem_key:
pkey = RSA.importKey(parsed_pem_key)
else:
raise NotImplementedError(
'No key in PEM format was detected. This implementation '
'can only use the PyCrypto library for keys in PEM '
'format.')
return PyCryptoSigner(pkey)

File diff suppressed because it is too large Load Diff

View File

@@ -1,173 +0,0 @@
# Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for reading OAuth 2.0 client secret files.
A client_secrets.json file contains all the information needed to interact with
an OAuth 2.0 protected service.
"""
import json
import six
# Properties that make a client_secrets.json file valid.
TYPE_WEB = 'web'
TYPE_INSTALLED = 'installed'
VALID_CLIENT = {
TYPE_WEB: {
'required': [
'client_id',
'client_secret',
'redirect_uris',
'auth_uri',
'token_uri',
],
'string': [
'client_id',
'client_secret',
],
},
TYPE_INSTALLED: {
'required': [
'client_id',
'client_secret',
'redirect_uris',
'auth_uri',
'token_uri',
],
'string': [
'client_id',
'client_secret',
],
},
}
class Error(Exception):
"""Base error for this module."""
class InvalidClientSecretsError(Error):
"""Format of ClientSecrets file is invalid."""
def _validate_clientsecrets(clientsecrets_dict):
"""Validate parsed client secrets from a file.
Args:
clientsecrets_dict: dict, a dictionary holding the client secrets.
Returns:
tuple, a string of the client type and the information parsed
from the file.
"""
_INVALID_FILE_FORMAT_MSG = (
'Invalid file format. See '
'https://developers.google.com/api-client-library/'
'python/guide/aaa_client_secrets')
if clientsecrets_dict is None:
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
try:
(client_type, client_info), = clientsecrets_dict.items()
except (ValueError, AttributeError):
raise InvalidClientSecretsError(
_INVALID_FILE_FORMAT_MSG + ' '
'Expected a JSON object with a single property for a "web" or '
'"installed" application')
if client_type not in VALID_CLIENT:
raise InvalidClientSecretsError(
'Unknown client type: {0}.'.format(client_type))
for prop_name in VALID_CLIENT[client_type]['required']:
if prop_name not in client_info:
raise InvalidClientSecretsError(
'Missing property "{0}" in a client type of "{1}".'.format(
prop_name, client_type))
for prop_name in VALID_CLIENT[client_type]['string']:
if client_info[prop_name].startswith('[['):
raise InvalidClientSecretsError(
'Property "{0}" is not configured.'.format(prop_name))
return client_type, client_info
def load(fp):
obj = json.load(fp)
return _validate_clientsecrets(obj)
def loads(s):
obj = json.loads(s)
return _validate_clientsecrets(obj)
def _loadfile(filename):
try:
with open(filename, 'r') as fp:
obj = json.load(fp)
except IOError as exc:
raise InvalidClientSecretsError('Error opening file', exc.filename,
exc.strerror, exc.errno)
return _validate_clientsecrets(obj)
def loadfile(filename, cache=None):
"""Loading of client_secrets JSON file, optionally backed by a cache.
Typical cache storage would be App Engine memcache service,
but you can pass in any other cache client that implements
these methods:
* ``get(key, namespace=ns)``
* ``set(key, value, namespace=ns)``
Usage::
# without caching
client_type, client_info = loadfile('secrets.json')
# using App Engine memcache service
from google.appengine.api import memcache
client_type, client_info = loadfile('secrets.json', cache=memcache)
Args:
filename: string, Path to a client_secrets.json file on a filesystem.
cache: An optional cache service client that implements get() and set()
methods. If not specified, the file is always being loaded from
a filesystem.
Raises:
InvalidClientSecretsError: In case of a validation error or some
I/O failure. Can happen only on cache miss.
Returns:
(client_type, client_info) tuple, as _loadfile() normally would.
JSON contents is validated only during first load. Cache hits are not
validated.
"""
_SECRET_NAMESPACE = 'oauth2client:secrets#ns'
if not cache:
return _loadfile(filename)
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
if obj is None:
client_type, client_info = _loadfile(filename)
obj = {client_type: client_info}
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
return next(six.iteritems(obj))

View File

@@ -1,6 +0,0 @@
"""Contributed modules.
Contrib contains modules that are not considered part of the core oauth2client
library but provide additional functionality. These modules are intended to
make it easier to use oauth2client.
"""

View File

@@ -1,163 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Google App Engine utilities helper.
Classes that directly require App Engine's ndb library. Provided
as a separate module in case of failure to import ndb while
other App Engine libraries are present.
"""
import logging
from google.appengine.ext import ndb
from oauth2client import client
NDB_KEY = ndb.Key
"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
NDB_MODEL = ndb.Model
"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
_LOGGER = logging.getLogger(__name__)
class SiteXsrfSecretKeyNDB(ndb.Model):
"""NDB Model for storage for the sites XSRF secret key.
Since this model uses the same kind as SiteXsrfSecretKey, it can be
used interchangeably. This simply provides an NDB model for interacting
with the same data the DB model interacts with.
There should only be one instance stored of this model, the one used
for the site.
"""
secret = ndb.StringProperty()
@classmethod
def _get_kind(cls):
"""Return the kind name for this class."""
return 'SiteXsrfSecretKey'
class FlowNDBProperty(ndb.PickleProperty):
"""App Engine NDB datastore Property for Flow.
Serves the same purpose as the DB FlowProperty, but for NDB models.
Since PickleProperty inherits from BlobProperty, the underlying
representation of the data in the datastore will be the same as in the
DB case.
Utility property that allows easy storage and retrieval of an
oauth2client.Flow
"""
def _validate(self, value):
"""Validates a value as a proper Flow object.
Args:
value: A value to be set on the property.
Raises:
TypeError if the value is not an instance of Flow.
"""
_LOGGER.info('validate: Got type %s', type(value))
if value is not None and not isinstance(value, client.Flow):
raise TypeError(
'Property {0} must be convertible to a flow '
'instance; received: {1}.'.format(self._name, value))
class CredentialsNDBProperty(ndb.BlobProperty):
"""App Engine NDB datastore Property for Credentials.
Serves the same purpose as the DB CredentialsProperty, but for NDB
models. Since CredentialsProperty stores data as a blob and this
inherits from BlobProperty, the data in the datastore will be the same
as in the DB case.
Utility property that allows easy storage and retrieval of Credentials
and subclasses.
"""
def _validate(self, value):
"""Validates a value as a proper credentials object.
Args:
value: A value to be set on the property.
Raises:
TypeError if the value is not an instance of Credentials.
"""
_LOGGER.info('validate: Got type %s', type(value))
if value is not None and not isinstance(value, client.Credentials):
raise TypeError(
'Property {0} must be convertible to a credentials '
'instance; received: {1}.'.format(self._name, value))
def _to_base_type(self, value):
"""Converts our validated value to a JSON serialized string.
Args:
value: A value to be set in the datastore.
Returns:
A JSON serialized version of the credential, else '' if value
is None.
"""
if value is None:
return ''
else:
return value.to_json()
def _from_base_type(self, value):
"""Converts our stored JSON string back to the desired type.
Args:
value: A value from the datastore to be converted to the
desired type.
Returns:
A deserialized Credentials (or subclass) object, else None if
the value can't be parsed.
"""
if not value:
return None
try:
# Uses the from_json method of the implied class of value
credentials = client.Credentials.new_from_json(value)
except ValueError:
credentials = None
return credentials
class CredentialsNDBModel(ndb.Model):
"""NDB Model for storage of OAuth 2.0 Credentials
Since this model uses the same kind as CredentialsModel and has a
property which can serialize and deserialize Credentials correctly, it
can be used interchangeably with a CredentialsModel to access, insert
and delete the same entities. This simply provides an NDB model for
interacting with the same data the DB model interacts with.
Storage of the model is keyed by the user.user_id().
"""
credentials = CredentialsNDBProperty()
@classmethod
def _get_kind(cls):
"""Return the kind name for this class."""
return 'CredentialsModel'

View File

@@ -1,118 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides helper methods for talking to the Compute Engine metadata server.
See https://cloud.google.com/compute/docs/metadata
"""
import datetime
import json
import os
from six.moves import http_client
from six.moves.urllib import parse as urlparse
from oauth2client import _helpers
from oauth2client import client
from oauth2client import transport
METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format(
os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal'))
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
def get(http, path, root=METADATA_ROOT, recursive=None):
"""Fetch a resource from the metadata server.
Args:
http: an object to be used to make HTTP requests.
path: A string indicating the resource to retrieve. For example,
'instance/service-accounts/default'
root: A string indicating the full path to the metadata server root.
recursive: A boolean indicating whether to do a recursive query of
metadata. See
https://cloud.google.com/compute/docs/metadata#aggcontents
Returns:
A dictionary if the metadata server returns JSON, otherwise a string.
Raises:
http_client.HTTPException if an error corrured while
retrieving metadata.
"""
url = urlparse.urljoin(root, path)
url = _helpers._add_query_parameter(url, 'recursive', recursive)
response, content = transport.request(
http, url, headers=METADATA_HEADERS)
if response.status == http_client.OK:
decoded = _helpers._from_bytes(content)
if response['content-type'] == 'application/json':
return json.loads(decoded)
else:
return decoded
else:
raise http_client.HTTPException(
'Failed to retrieve {0} from the Google Compute Engine'
'metadata service. Response:\n{1}'.format(url, response))
def get_service_account_info(http, service_account='default'):
"""Get information about a service account from the metadata server.
Args:
http: an object to be used to make HTTP requests.
service_account: An email specifying the service account for which to
look up information. Default will be information for the "default"
service account of the current compute engine instance.
Returns:
A dictionary with information about the specified service account,
for example:
{
'email': '...',
'scopes': ['scope', ...],
'aliases': ['default', '...']
}
"""
return get(
http,
'instance/service-accounts/{0}/'.format(service_account),
recursive=True)
def get_token(http, service_account='default'):
"""Fetch an oauth token for the
Args:
http: an object to be used to make HTTP requests.
service_account: An email specifying the service account this token
should represent. Default will be a token for the "default" service
account of the current compute engine instance.
Returns:
A tuple of (access token, token expiration), where access token is the
access token as a string and token expiration is a datetime object
that indicates when the access token will expire.
"""
token_json = get(
http,
'instance/service-accounts/{0}/token'.format(service_account))
token_expiry = client._UTCNOW() + datetime.timedelta(
seconds=token_json['expires_in'])
return token_json['access_token'], token_expiry

View File

@@ -1,910 +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.
"""Utilities for Google App Engine
Utilities for making it easier to use OAuth 2.0 on Google App Engine.
"""
import cgi
import json
import logging
import os
import pickle
import threading
from google.appengine.api import app_identity
from google.appengine.api import memcache
from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext.webapp.util import login_required
import webapp2 as webapp
import oauth2client
from oauth2client import _helpers
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client.contrib import xsrfutil
# This is a temporary fix for a Google internal issue.
try:
from oauth2client.contrib import _appengine_ndb
except ImportError: # pragma: NO COVER
_appengine_ndb = None
logger = logging.getLogger(__name__)
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
XSRF_MEMCACHE_ID = 'xsrf_secret_key'
if _appengine_ndb is None: # pragma: NO COVER
CredentialsNDBModel = None
CredentialsNDBProperty = None
FlowNDBProperty = None
_NDB_KEY = None
_NDB_MODEL = None
SiteXsrfSecretKeyNDB = None
else:
CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
FlowNDBProperty = _appengine_ndb.FlowNDBProperty
_NDB_KEY = _appengine_ndb.NDB_KEY
_NDB_MODEL = _appengine_ndb.NDB_MODEL
SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
def _safe_html(s):
"""Escape text to make it safe to display.
Args:
s: string, The text to escape.
Returns:
The escaped text as a string.
"""
return cgi.escape(s, quote=1).replace("'", '&#39;')
class SiteXsrfSecretKey(db.Model):
"""Storage for the sites XSRF secret key.
There will only be one instance stored of this model, the one used for the
site.
"""
secret = db.StringProperty()
def _generate_new_xsrf_secret_key():
"""Returns a random XSRF secret key."""
return os.urandom(16).encode("hex")
def xsrf_secret_key():
"""Return the secret key for use for XSRF protection.
If the Site entity does not have a secret key, this method will also create
one and persist it.
Returns:
The secret key.
"""
secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
if not secret:
# Load the one and only instance of SiteXsrfSecretKey.
model = SiteXsrfSecretKey.get_or_insert(key_name='site')
if not model.secret:
model.secret = _generate_new_xsrf_secret_key()
model.put()
secret = model.secret
memcache.add(XSRF_MEMCACHE_ID, secret,
namespace=OAUTH2CLIENT_NAMESPACE)
return str(secret)
class AppAssertionCredentials(client.AssertionCredentials):
"""Credentials object for App Engine Assertion Grants
This object will allow an App Engine application to identify itself to
Google and other OAuth 2.0 servers that can verify assertions. It can be
used for the purpose of accessing data stored under an account assigned to
the App Engine application itself.
This credential does not require a flow to instantiate because it
represents a two legged flow, and therefore has all of the required
information to generate and refresh its own access tokens.
"""
@_helpers.positional(2)
def __init__(self, scope, **kwargs):
"""Constructor for AppAssertionCredentials
Args:
scope: string or iterable of strings, scope(s) of the credentials
being requested.
**kwargs: optional keyword args, including:
service_account_id: service account id of the application. If None
or unspecified, the default service account for
the app is used.
"""
self.scope = _helpers.scopes_to_string(scope)
self._kwargs = kwargs
self.service_account_id = kwargs.get('service_account_id', None)
self._service_account_email = None
# Assertion type is no longer used, but still in the
# parent class signature.
super(AppAssertionCredentials, self).__init__(None)
@classmethod
def from_json(cls, json_data):
data = json.loads(json_data)
return AppAssertionCredentials(data['scope'])
def _refresh(self, http):
"""Refreshes the access token.
Since the underlying App Engine app_identity implementation does its
own caching we can skip all the storage hoops and just to a refresh
using the API.
Args:
http: unused HTTP object
Raises:
AccessTokenRefreshError: When the refresh fails.
"""
try:
scopes = self.scope.split()
(token, _) = app_identity.get_access_token(
scopes, service_account_id=self.service_account_id)
except app_identity.Error as e:
raise client.AccessTokenRefreshError(str(e))
self.access_token = token
@property
def serialization_data(self):
raise NotImplementedError('Cannot serialize credentials '
'for Google App Engine.')
def create_scoped_required(self):
return not self.scope
def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self._kwargs)
def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
Implements abstract method
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
Args:
blob: bytes, Message to be signed.
Returns:
tuple, A pair of the private key ID used to sign the blob and
the signed contents.
"""
return app_identity.sign_blob(blob)
@property
def service_account_email(self):
"""Get the email for the current service account.
Returns:
string, The email associated with the Google App Engine
service account.
"""
if self._service_account_email is None:
self._service_account_email = (
app_identity.get_service_account_name())
return self._service_account_email
class FlowProperty(db.Property):
"""App Engine datastore Property for Flow.
Utility property that allows easy storage and retrieval of an
oauth2client.Flow
"""
# Tell what the user type is.
data_type = client.Flow
# For writing to datastore.
def get_value_for_datastore(self, model_instance):
flow = super(FlowProperty, self).get_value_for_datastore(
model_instance)
return db.Blob(pickle.dumps(flow))
# For reading from datastore.
def make_value_from_datastore(self, value):
if value is None:
return None
return pickle.loads(value)
def validate(self, value):
if value is not None and not isinstance(value, client.Flow):
raise db.BadValueError(
'Property {0} must be convertible '
'to a FlowThreeLegged instance ({1})'.format(self.name, value))
return super(FlowProperty, self).validate(value)
def empty(self, value):
return not value
class CredentialsProperty(db.Property):
"""App Engine datastore Property for Credentials.
Utility property that allows easy storage and retrieval of
oauth2client.Credentials
"""
# Tell what the user type is.
data_type = client.Credentials
# For writing to datastore.
def get_value_for_datastore(self, model_instance):
logger.info("get: Got type " + str(type(model_instance)))
cred = super(CredentialsProperty, self).get_value_for_datastore(
model_instance)
if cred is None:
cred = ''
else:
cred = cred.to_json()
return db.Blob(cred)
# For reading from datastore.
def make_value_from_datastore(self, value):
logger.info("make: Got type " + str(type(value)))
if value is None:
return None
if len(value) == 0:
return None
try:
credentials = client.Credentials.new_from_json(value)
except ValueError:
credentials = None
return credentials
def validate(self, value):
value = super(CredentialsProperty, self).validate(value)
logger.info("validate: Got type " + str(type(value)))
if value is not None and not isinstance(value, client.Credentials):
raise db.BadValueError(
'Property {0} must be convertible '
'to a Credentials instance ({1})'.format(self.name, value))
return value
class StorageByKeyName(client.Storage):
"""Store and retrieve a credential to and from the App Engine datastore.
This Storage helper presumes the Credentials have been stored as a
CredentialsProperty or CredentialsNDBProperty on a datastore model class,
and that entities are stored by key_name.
"""
@_helpers.positional(4)
def __init__(self, model, key_name, property_name, cache=None, user=None):
"""Constructor for Storage.
Args:
model: db.Model or ndb.Model, model class
key_name: string, key name for the entity that has the credentials
property_name: string, name of the property that is a
CredentialsProperty or CredentialsNDBProperty.
cache: memcache, a write-through cache to put in front of the
datastore. If the model you are using is an NDB model, using
a cache will be redundant since the model uses an instance
cache and memcache for you.
user: users.User object, optional. Can be used to grab user ID as a
key_name if no key name is specified.
"""
super(StorageByKeyName, self).__init__()
if key_name is None:
if user is None:
raise ValueError('StorageByKeyName called with no '
'key name or user.')
key_name = user.user_id()
self._model = model
self._key_name = key_name
self._property_name = property_name
self._cache = cache
def _is_ndb(self):
"""Determine whether the model of the instance is an NDB model.
Returns:
Boolean indicating whether or not the model is an NDB or DB model.
"""
# issubclass will fail if one of the arguments is not a class, only
# need worry about new-style classes since ndb and db models are
# new-style
if isinstance(self._model, type):
if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
return True
elif issubclass(self._model, db.Model):
return False
raise TypeError(
'Model class not an NDB or DB model: {0}.'.format(self._model))
def _get_entity(self):
"""Retrieve entity from datastore.
Uses a different model method for db or ndb models.
Returns:
Instance of the model corresponding to the current storage object
and stored using the key name of the storage object.
"""
if self._is_ndb():
return self._model.get_by_id(self._key_name)
else:
return self._model.get_by_key_name(self._key_name)
def _delete_entity(self):
"""Delete entity from datastore.
Attempts to delete using the key_name stored on the object, whether or
not the given key is in the datastore.
"""
if self._is_ndb():
_NDB_KEY(self._model, self._key_name).delete()
else:
entity_key = db.Key.from_path(self._model.kind(), self._key_name)
db.delete(entity_key)
@db.non_transactional(allow_existing=True)
def locked_get(self):
"""Retrieve Credential from datastore.
Returns:
oauth2client.Credentials
"""
credentials = None
if self._cache:
json = self._cache.get(self._key_name)
if json:
credentials = client.Credentials.new_from_json(json)
if credentials is None:
entity = self._get_entity()
if entity is not None:
credentials = getattr(entity, self._property_name)
if self._cache:
self._cache.set(self._key_name, credentials.to_json())
if credentials and hasattr(credentials, 'set_store'):
credentials.set_store(self)
return credentials
@db.non_transactional(allow_existing=True)
def locked_put(self, credentials):
"""Write a Credentials to the datastore.
Args:
credentials: Credentials, the credentials to store.
"""
entity = self._model.get_or_insert(self._key_name)
setattr(entity, self._property_name, credentials)
entity.put()
if self._cache:
self._cache.set(self._key_name, credentials.to_json())
@db.non_transactional(allow_existing=True)
def locked_delete(self):
"""Delete Credential from datastore."""
if self._cache:
self._cache.delete(self._key_name)
self._delete_entity()
class CredentialsModel(db.Model):
"""Storage for OAuth 2.0 Credentials
Storage of the model is keyed by the user.user_id().
"""
credentials = CredentialsProperty()
def _build_state_value(request_handler, user):
"""Composes the value for the 'state' parameter.
Packs the current request URI and an XSRF token into an opaque string that
can be passed to the authentication server via the 'state' parameter.
Args:
request_handler: webapp.RequestHandler, The request.
user: google.appengine.api.users.User, The current user.
Returns:
The state value as a string.
"""
uri = request_handler.request.url
token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
action_id=str(uri))
return uri + ':' + token
def _parse_state_value(state, user):
"""Parse the value of the 'state' parameter.
Parses the value and validates the XSRF token in the state parameter.
Args:
state: string, The value of the state parameter.
user: google.appengine.api.users.User, The current user.
Returns:
The redirect URI, or None if XSRF token is not valid.
"""
uri, token = state.rsplit(':', 1)
if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
action_id=uri):
return uri
else:
return None
class OAuth2Decorator(object):
"""Utility for making OAuth 2.0 easier.
Instantiate and then use with oauth_required or oauth_aware
as decorators on webapp.RequestHandler methods.
::
decorator = OAuth2Decorator(
client_id='837...ent.com',
client_secret='Qh...wwI',
scope='https://www.googleapis.com/auth/plus')
class MainHandler(webapp.RequestHandler):
@decorator.oauth_required
def get(self):
http = decorator.http()
# http is authorized with the user's Credentials and can be
# used in API calls
"""
def set_credentials(self, credentials):
self._tls.credentials = credentials
def get_credentials(self):
"""A thread local Credentials object.
Returns:
A client.Credentials object, or None if credentials hasn't been set
in this thread yet, which may happen when calling has_credentials
inside oauth_aware.
"""
return getattr(self._tls, 'credentials', None)
credentials = property(get_credentials, set_credentials)
def set_flow(self, flow):
self._tls.flow = flow
def get_flow(self):
"""A thread local Flow object.
Returns:
A credentials.Flow object, or None if the flow hasn't been set in
this thread yet, which happens in _create_flow() since Flows are
created lazily.
"""
return getattr(self._tls, 'flow', None)
flow = property(get_flow, set_flow)
@_helpers.positional(4)
def __init__(self, client_id, client_secret, scope,
auth_uri=oauth2client.GOOGLE_AUTH_URI,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
user_agent=None,
message=None,
callback_path='/oauth2callback',
token_response_param=None,
_storage_class=StorageByKeyName,
_credentials_class=CredentialsModel,
_credentials_property_name='credentials',
**kwargs):
"""Constructor for OAuth2Decorator
Args:
client_id: string, client identifier.
client_secret: string client secret.
scope: string or iterable of strings, scope(s) of the credentials
being requested.
auth_uri: string, URI for authorization endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider
can be used.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0
provider can be used.
user_agent: string, User agent of your application, default to
None.
message: Message to display if there are problems with the
OAuth 2.0 configuration. The message may contain HTML and
will be presented on the web interface for any method that
uses the decorator.
callback_path: string, The absolute path to use as the callback
URI. Note that this must match up with the URI given
when registering the application in the APIs
Console.
token_response_param: string. If provided, the full JSON response
to the access token request will be encoded
and included in this query parameter in the
callback URI. This is useful with providers
(e.g. wordpress.com) that include extra
fields that the client may want.
_storage_class: "Protected" keyword argument not typically provided
to this constructor. A storage class to aid in
storing a Credentials object for a user in the
datastore. Defaults to StorageByKeyName.
_credentials_class: "Protected" keyword argument not typically
provided to this constructor. A db or ndb Model
class to hold credentials. Defaults to
CredentialsModel.
_credentials_property_name: "Protected" keyword argument not
typically provided to this constructor.
A string indicating the name of the
field on the _credentials_class where a
Credentials object will be stored.
Defaults to 'credentials'.
**kwargs: dict, Keyword arguments are passed along as kwargs to
the OAuth2WebServerFlow constructor.
"""
self._tls = threading.local()
self.flow = None
self.credentials = None
self._client_id = client_id
self._client_secret = client_secret
self._scope = _helpers.scopes_to_string(scope)
self._auth_uri = auth_uri
self._token_uri = token_uri
self._revoke_uri = revoke_uri
self._user_agent = user_agent
self._kwargs = kwargs
self._message = message
self._in_error = False
self._callback_path = callback_path
self._token_response_param = token_response_param
self._storage_class = _storage_class
self._credentials_class = _credentials_class
self._credentials_property_name = _credentials_property_name
def _display_error_message(self, request_handler):
request_handler.response.out.write('<html><body>')
request_handler.response.out.write(_safe_html(self._message))
request_handler.response.out.write('</body></html>')
def oauth_required(self, method):
"""Decorator that starts the OAuth 2.0 dance.
Starts the OAuth dance for the logged in user if they haven't already
granted access for this application.
Args:
method: callable, to be decorated method of a webapp.RequestHandler
instance.
"""
def check_oauth(request_handler, *args, **kwargs):
if self._in_error:
self._display_error_message(request_handler)
return
user = users.get_current_user()
# Don't use @login_decorator as this could be used in a
# POST request.
if not user:
request_handler.redirect(users.create_login_url(
request_handler.request.uri))
return
self._create_flow(request_handler)
# Store the request URI in 'state' so we can use it later
self.flow.params['state'] = _build_state_value(
request_handler, user)
self.credentials = self._storage_class(
self._credentials_class, None,
self._credentials_property_name, user=user).get()
if not self.has_credentials():
return request_handler.redirect(self.authorize_url())
try:
resp = method(request_handler, *args, **kwargs)
except client.AccessTokenRefreshError:
return request_handler.redirect(self.authorize_url())
finally:
self.credentials = None
return resp
return check_oauth
def _create_flow(self, request_handler):
"""Create the Flow object.
The Flow is calculated lazily since we don't know where this app is
running until it receives a request, at which point redirect_uri can be
calculated and then the Flow object can be constructed.
Args:
request_handler: webapp.RequestHandler, the request handler.
"""
if self.flow is None:
redirect_uri = request_handler.request.relative_url(
self._callback_path) # Usually /oauth2callback
self.flow = client.OAuth2WebServerFlow(
self._client_id, self._client_secret, self._scope,
redirect_uri=redirect_uri, user_agent=self._user_agent,
auth_uri=self._auth_uri, token_uri=self._token_uri,
revoke_uri=self._revoke_uri, **self._kwargs)
def oauth_aware(self, method):
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
Does all the setup for the OAuth dance, but doesn't initiate it.
This decorator is useful if you want to create a page that knows
whether or not the user has granted access to this application.
From within a method decorated with @oauth_aware the has_credentials()
and authorize_url() methods can be called.
Args:
method: callable, to be decorated method of a webapp.RequestHandler
instance.
"""
def setup_oauth(request_handler, *args, **kwargs):
if self._in_error:
self._display_error_message(request_handler)
return
user = users.get_current_user()
# Don't use @login_decorator as this could be used in a
# POST request.
if not user:
request_handler.redirect(users.create_login_url(
request_handler.request.uri))
return
self._create_flow(request_handler)
self.flow.params['state'] = _build_state_value(request_handler,
user)
self.credentials = self._storage_class(
self._credentials_class, None,
self._credentials_property_name, user=user).get()
try:
resp = method(request_handler, *args, **kwargs)
finally:
self.credentials = None
return resp
return setup_oauth
def has_credentials(self):
"""True if for the logged in user there are valid access Credentials.
Must only be called from with a webapp.RequestHandler subclassed method
that had been decorated with either @oauth_required or @oauth_aware.
"""
return self.credentials is not None and not self.credentials.invalid
def authorize_url(self):
"""Returns the URL to start the OAuth dance.
Must only be called from with a webapp.RequestHandler subclassed method
that had been decorated with either @oauth_required or @oauth_aware.
"""
url = self.flow.step1_get_authorize_url()
return str(url)
def http(self, *args, **kwargs):
"""Returns an authorized http instance.
Must only be called from within an @oauth_required decorated method, or
from within an @oauth_aware decorated method where has_credentials()
returns True.
Args:
*args: Positional arguments passed to httplib2.Http constructor.
**kwargs: Positional arguments passed to httplib2.Http constructor.
"""
return self.credentials.authorize(
transport.get_http_object(*args, **kwargs))
@property
def callback_path(self):
"""The absolute path where the callback will occur.
Note this is the absolute path, not the absolute URI, that will be
calculated by the decorator at runtime. See callback_handler() for how
this should be used.
Returns:
The callback path as a string.
"""
return self._callback_path
def callback_handler(self):
"""RequestHandler for the OAuth 2.0 redirect callback.
Usage::
app = webapp.WSGIApplication([
('/index', MyIndexHandler),
...,
(decorator.callback_path, decorator.callback_handler())
])
Returns:
A webapp.RequestHandler that handles the redirect back from the
server during the OAuth 2.0 dance.
"""
decorator = self
class OAuth2Handler(webapp.RequestHandler):
"""Handler for the redirect_uri of the OAuth 2.0 dance."""
@login_required
def get(self):
error = self.request.get('error')
if error:
errormsg = self.request.get('error_description', error)
self.response.out.write(
'The authorization request failed: {0}'.format(
_safe_html(errormsg)))
else:
user = users.get_current_user()
decorator._create_flow(self)
credentials = decorator.flow.step2_exchange(
self.request.params)
decorator._storage_class(
decorator._credentials_class, None,
decorator._credentials_property_name,
user=user).put(credentials)
redirect_uri = _parse_state_value(
str(self.request.get('state')), user)
if redirect_uri is None:
self.response.out.write(
'The authorization request failed')
return
if (decorator._token_response_param and
credentials.token_response):
resp_json = json.dumps(credentials.token_response)
redirect_uri = _helpers._add_query_parameter(
redirect_uri, decorator._token_response_param,
resp_json)
self.redirect(redirect_uri)
return OAuth2Handler
def callback_application(self):
"""WSGI application for handling the OAuth 2.0 redirect callback.
If you need finer grained control use `callback_handler` which returns
just the webapp.RequestHandler.
Returns:
A webapp.WSGIApplication that handles the redirect back from the
server during the OAuth 2.0 dance.
"""
return webapp.WSGIApplication([
(self.callback_path, self.callback_handler())
])
class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
"""An OAuth2Decorator that builds from a clientsecrets file.
Uses a clientsecrets file as the source for all the information when
constructing an OAuth2Decorator.
::
decorator = OAuth2DecoratorFromClientSecrets(
os.path.join(os.path.dirname(__file__), 'client_secrets.json')
scope='https://www.googleapis.com/auth/plus')
class MainHandler(webapp.RequestHandler):
@decorator.oauth_required
def get(self):
http = decorator.http()
# http is authorized with the user's Credentials and can be
# used in API calls
"""
@_helpers.positional(3)
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
"""Constructor
Args:
filename: string, File name of client secrets.
scope: string or iterable of strings, scope(s) of the credentials
being requested.
message: string, A friendly string to display to the user if the
clientsecrets file is missing or invalid. The message may
contain HTML and will be presented on the web interface
for any method that uses the decorator.
cache: An optional cache service client that implements get() and
set()
methods. See clientsecrets.loadfile() for details.
**kwargs: dict, Keyword arguments are passed along as kwargs to
the OAuth2WebServerFlow constructor.
"""
client_type, client_info = clientsecrets.loadfile(filename,
cache=cache)
if client_type not in (clientsecrets.TYPE_WEB,
clientsecrets.TYPE_INSTALLED):
raise clientsecrets.InvalidClientSecretsError(
"OAuth2Decorator doesn't support this OAuth 2.0 flow.")
constructor_kwargs = dict(kwargs)
constructor_kwargs.update({
'auth_uri': client_info['auth_uri'],
'token_uri': client_info['token_uri'],
'message': message,
})
revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None:
constructor_kwargs['revoke_uri'] = revoke_uri
super(OAuth2DecoratorFromClientSecrets, self).__init__(
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)
if message is not None:
self._message = message
else:
self._message = 'Please configure your application for OAuth 2.0.'
@_helpers.positional(2)
def oauth2decorator_from_clientsecrets(filename, scope,
message=None, cache=None):
"""Creates an OAuth2Decorator populated from a clientsecrets file.
Args:
filename: string, File name of client secrets.
scope: string or list of strings, scope(s) of the credentials being
requested.
message: string, A friendly string to display to the user if the
clientsecrets file is missing or invalid. The message may
contain HTML and will be presented on the web interface for
any method that uses the decorator.
cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
Returns: An OAuth2Decorator
"""
return OAuth2DecoratorFromClientSecrets(filename, scope,
message=message, cache=cache)

View File

@@ -1,152 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""OAuth 2.0 utitilies for Google Developer Shell environment."""
import datetime
import json
import os
import socket
from oauth2client import _helpers
from oauth2client import client
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
class Error(Exception):
"""Errors for this module."""
pass
class CommunicationError(Error):
"""Errors for communication with the Developer Shell server."""
class NoDevshellServer(Error):
"""Error when no Developer Shell server can be contacted."""
# The request for credential information to the Developer Shell client socket
# is always an empty PBLite-formatted JSON object, so just define it as a
# constant.
CREDENTIAL_INFO_REQUEST_JSON = '[]'
class CredentialInfoResponse(object):
"""Credential information response from Developer Shell server.
The credential information response from Developer Shell socket is a
PBLite-formatted JSON array with fields encoded by their index in the
array:
* Index 0 - user email
* Index 1 - default project ID. None if the project context is not known.
* Index 2 - OAuth2 access token. None if there is no valid auth context.
* Index 3 - Seconds until the access token expires. None if not present.
"""
def __init__(self, json_string):
"""Initialize the response data from JSON PBLite array."""
pbl = json.loads(json_string)
if not isinstance(pbl, list):
raise ValueError('Not a list: ' + str(pbl))
pbl_len = len(pbl)
self.user_email = pbl[0] if pbl_len > 0 else None
self.project_id = pbl[1] if pbl_len > 1 else None
self.access_token = pbl[2] if pbl_len > 2 else None
self.expires_in = pbl[3] if pbl_len > 3 else None
def _SendRecv():
"""Communicate with the Developer Shell server socket."""
port = int(os.getenv(DEVSHELL_ENV, 0))
if port == 0:
raise NoDevshellServer()
sock = socket.socket()
sock.connect(('localhost', port))
data = CREDENTIAL_INFO_REQUEST_JSON
msg = '{0}\n{1}'.format(len(data), data)
sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
header = sock.recv(6).decode()
if '\n' not in header:
raise CommunicationError('saw no newline in the first 6 bytes')
len_str, json_str = header.split('\n', 1)
to_read = int(len_str) - len(json_str)
if to_read > 0:
json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
return CredentialInfoResponse(json_str)
class DevshellCredentials(client.GoogleCredentials):
"""Credentials object for Google Developer Shell environment.
This object will allow a Google Developer Shell session to identify its
user to Google and other OAuth 2.0 servers that can verify assertions. It
can be used for the purpose of accessing data stored under the user
account.
This credential does not require a flow to instantiate because it
represents a two legged flow, and therefore has all of the required
information to generate and refresh its own access tokens.
"""
def __init__(self, user_agent=None):
super(DevshellCredentials, self).__init__(
None, # access_token, initialized below
None, # client_id
None, # client_secret
None, # refresh_token
None, # token_expiry
None, # token_uri
user_agent)
self._refresh(None)
def _refresh(self, http):
"""Refreshes the access token.
Args:
http: unused HTTP object
"""
self.devshell_response = _SendRecv()
self.access_token = self.devshell_response.access_token
expires_in = self.devshell_response.expires_in
if expires_in is not None:
delta = datetime.timedelta(seconds=expires_in)
self.token_expiry = client._UTCNOW() + delta
else:
self.token_expiry = None
@property
def user_email(self):
return self.devshell_response.user_email
@property
def project_id(self):
return self.devshell_response.project_id
@classmethod
def from_json(cls, json_data):
raise NotImplementedError(
'Cannot load Developer Shell credentials from JSON.')
@property
def serialization_data(self):
raise NotImplementedError(
'Cannot serialize Developer Shell credentials.')

View File

@@ -1,65 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Dictionary storage for OAuth2 Credentials."""
from oauth2client import client
class DictionaryStorage(client.Storage):
"""Store and retrieve credentials to and from a dictionary-like object.
Args:
dictionary: A dictionary or dictionary-like object.
key: A string or other hashable. The credentials will be stored in
``dictionary[key]``.
lock: An optional threading.Lock-like object. The lock will be
acquired before anything is written or read from the
dictionary.
"""
def __init__(self, dictionary, key, lock=None):
"""Construct a DictionaryStorage instance."""
super(DictionaryStorage, self).__init__(lock=lock)
self._dictionary = dictionary
self._key = key
def locked_get(self):
"""Retrieve the credentials from the dictionary, if they exist.
Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
"""
serialized = self._dictionary.get(self._key)
if serialized is None:
return None
credentials = client.OAuth2Credentials.from_json(serialized)
credentials.set_store(self)
return credentials
def locked_put(self, credentials):
"""Save the credentials to the dictionary.
Args:
credentials: A :class:`oauth2client.client.OAuth2Credentials`
instance.
"""
serialized = credentials.to_json()
self._dictionary[self._key] = serialized
def locked_delete(self):
"""Remove the credentials from the dictionary, if they exist."""
self._dictionary.pop(self._key, None)

View File

@@ -1,489 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for the Django web framework.
Provides Django views and helpers the make using the OAuth2 web server
flow easier. It includes an ``oauth_required`` decorator to automatically
ensure that user credentials are available, and an ``oauth_enabled`` decorator
to check if the user has authorized, and helper shortcuts to create the
authorization URL otherwise.
There are two basic use cases supported. The first is using Google OAuth as the
primary form of authentication, which is the simpler approach recommended
for applications without their own user system.
The second use case is adding Google OAuth credentials to an
existing Django model containing a Django user field. Most of the
configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in
settings.py. See "Adding Credentials To An Existing Django User System" for
usage differences.
Only Django versions 1.8+ are supported.
Configuration
===============
To configure, you'll need a set of OAuth2 web application credentials from
`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
Add the helper to your INSTALLED_APPS:
.. code-block:: python
:caption: settings.py
:name: installed_apps
INSTALLED_APPS = (
# other apps
"django.contrib.sessions.middleware"
"oauth2client.contrib.django_util"
)
This helper also requires the Django Session Middleware, so
``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
MIDDLEWARE or MIDDLEWARE_CLASSES (in Django versions <1.10) should also
contain the string 'django.contrib.sessions.middleware.SessionMiddleware'.
Add the client secrets created earlier to the settings. You can either
specify the path to the credentials file in JSON format
.. code-block:: python
:caption: settings.py
:name: secrets_file
GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
Or, directly configure the client Id and client secret.
.. code-block:: python
:caption: settings.py
:name: secrets_config
GOOGLE_OAUTH2_CLIENT_ID=client-id-field
GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
By default, the default scopes for the required decorator only contains the
``email`` scopes. You can change that default in the settings.
.. code-block:: python
:caption: settings.py
:name: scopes
GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
By default, the decorators will add an `oauth` object to the Django request
object, and include all of its state and helpers inside that object. If the
`oauth` name conflicts with another usage, it can be changed
.. code-block:: python
:caption: settings.py
:name: request_prefix
# changes request.oauth to request.google_oauth
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
Add the oauth2 routes to your application's urls.py urlpatterns.
.. code-block:: python
:caption: urls.py
:name: urls
from oauth2client.contrib.django_util.site import urls as oauth2_urls
urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
This creates a credentials object with an id_token, and allows you to create
an `http` object to build service clients with. These are all attached to the
request.oauth
.. code-block:: python
:caption: views.py
:name: views_required
from oauth2client.contrib.django_util.decorators import oauth_required
@oauth_required
def requires_default_scopes(request):
email = request.oauth.credentials.id_token['email']
service = build(serviceName='calendar', version='v3',
http=request.oauth.http,
developerKey=API_KEY)
events = service.events().list(calendarId='primary').execute()['items']
return HttpResponse("email: {0} , calendar: {1}".format(
email,str(events)))
return HttpResponse(
"email: {0} , calendar: {1}".format(email, str(events)))
To make OAuth2 optional and provide an authorization link in your own views.
.. code-block:: python
:caption: views.py
:name: views_enabled2
from oauth2client.contrib.django_util.decorators import oauth_enabled
@oauth_enabled
def optional_oauth2(request):
if request.oauth.has_credentials():
# this could be passed into a view
# request.oauth.http is also initialized
return HttpResponse("User email: {0}".format(
request.oauth.credentials.id_token['email']))
else:
return HttpResponse(
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
'</a>'.format(request.oauth.get_authorize_redirect()))
If a view needs a scope not included in the default scopes specified in
the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
and specify additional scopes in the decorator arguments.
.. code-block:: python
:caption: views.py
:name: views_required_additional_scopes
@oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
def drive_required(request):
if request.oauth.has_credentials():
service = build(serviceName='drive', version='v2',
http=request.oauth.http,
developerKey=API_KEY)
events = service.files().list().execute()['items']
return HttpResponse(str(events))
else:
return HttpResponse(
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
'</a>'.format(request.oauth.get_authorize_redirect()))
To provide a callback on authorization being completed, use the
oauth2_authorized signal:
.. code-block:: python
:caption: views.py
:name: signals
from oauth2client.contrib.django_util.signals import oauth2_authorized
def test_callback(sender, request, credentials, **kwargs):
print("Authorization Signal Received {0}".format(
credentials.id_token['email']))
oauth2_authorized.connect(test_callback)
Adding Credentials To An Existing Django User System
=====================================================
As an alternative to storing the credentials in the session, the helper
can be configured to store the fields on a Django model. This might be useful
if you need to use the credentials outside the context of a user request. It
also prevents the need for a logged in user to repeat the OAuth flow when
starting a new session.
To use, change ``settings.py``
.. code-block:: python
:caption: settings.py
:name: storage_model_config
GOOGLE_OAUTH2_STORAGE_MODEL = {
'model': 'path.to.model.MyModel',
'user_property': 'user_id',
'credentials_property': 'credential'
}
Where ``path.to.model`` class is the fully qualified name of a
``django.db.model`` class containing a ``django.contrib.auth.models.User``
field with the name specified by `user_property` and a
:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name
specified by `credentials_property`. For the sample configuration given,
our model would look like
.. code-block:: python
:caption: models.py
:name: storage_model_model
from django.contrib.auth.models import User
from oauth2client.contrib.django_util.models import CredentialsField
class MyModel(models.Model):
# ... other fields here ...
user = models.OneToOneField(User)
credential = CredentialsField()
"""
import importlib
import django.conf
from django.core import exceptions
from django.core import urlresolvers
from six.moves.urllib import parse
from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client.contrib import dictionary_storage
from oauth2client.contrib.django_util import storage
GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
def _load_client_secrets(filename):
"""Loads client secrets from the given filename.
Args:
filename: The name of the file containing the JSON secret key.
Returns:
A 2-tuple, the first item containing the client id, and the second
item containing a client secret.
"""
client_type, client_info = clientsecrets.loadfile(filename)
if client_type != clientsecrets.TYPE_WEB:
raise ValueError(
'The flow specified in {} is not supported, only the WEB flow '
'type is supported.'.format(client_type))
return client_info['client_id'], client_info['client_secret']
def _get_oauth2_client_id_and_secret(settings_instance):
"""Initializes client id and client secret based on the settings.
Args:
settings_instance: An instance of ``django.conf.settings``.
Returns:
A 2-tuple, the first item is the client id and the second
item is the client secret.
"""
secret_json = getattr(settings_instance,
'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
if secret_json is not None:
return _load_client_secrets(secret_json)
else:
client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
None)
client_secret = getattr(settings_instance,
"GOOGLE_OAUTH2_CLIENT_SECRET", None)
if client_id is not None and client_secret is not None:
return client_id, client_secret
else:
raise exceptions.ImproperlyConfigured(
"Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
"both GOOGLE_OAUTH2_CLIENT_ID and "
"GOOGLE_OAUTH2_CLIENT_SECRET in settings.py")
def _get_storage_model():
"""This configures whether the credentials will be stored in the session
or the Django ORM based on the settings. By default, the credentials
will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL`
is found in the settings. Usually, the ORM storage is used to integrate
credentials into an existing Django user system.
Returns:
A tuple containing three strings, or None. If
``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple
will contain the fully qualifed path of the `django.db.model`,
the name of the ``django.contrib.auth.models.User`` field on the
model, and the name of the
:class:`oauth2client.contrib.django_util.models.CredentialsField`
field on the model. If Django ORM storage is not configured,
this function returns None.
"""
storage_model_settings = getattr(django.conf.settings,
'GOOGLE_OAUTH2_STORAGE_MODEL', None)
if storage_model_settings is not None:
return (storage_model_settings['model'],
storage_model_settings['user_property'],
storage_model_settings['credentials_property'])
else:
return None, None, None
class OAuth2Settings(object):
"""Initializes Django OAuth2 Helper Settings
This class loads the OAuth2 Settings from the Django settings, and then
provides those settings as attributes to the rest of the views and
decorators in the module.
Attributes:
scopes: A list of OAuth2 scopes that the decorators and views will use
as defaults.
request_prefix: The name of the attribute that the decorators use to
attach the UserOAuth2 object to the Django request object.
client_id: The OAuth2 Client ID.
client_secret: The OAuth2 Client Secret.
"""
def __init__(self, settings_instance):
self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
GOOGLE_OAUTH2_DEFAULT_SCOPES)
self.request_prefix = getattr(settings_instance,
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
info = _get_oauth2_client_id_and_secret(settings_instance)
self.client_id, self.client_secret = info
# Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE
middleware_settings = getattr(settings_instance, 'MIDDLEWARE', None)
if middleware_settings is None:
middleware_settings = getattr(
settings_instance, 'MIDDLEWARE_CLASSES', None)
if middleware_settings is None:
raise exceptions.ImproperlyConfigured(
'Django settings has neither MIDDLEWARE nor MIDDLEWARE_CLASSES'
'configured')
if ('django.contrib.sessions.middleware.SessionMiddleware' not in
middleware_settings):
raise exceptions.ImproperlyConfigured(
'The Google OAuth2 Helper requires session middleware to '
'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE '
'setting to include \'django.contrib.sessions.middleware.'
'SessionMiddleware\'.')
(self.storage_model, self.storage_model_user_property,
self.storage_model_credentials_property) = _get_storage_model()
oauth2_settings = OAuth2Settings(django.conf.settings)
_CREDENTIALS_KEY = 'google_oauth2_credentials'
def get_storage(request):
""" Gets a Credentials storage object provided by the Django OAuth2 Helper
object.
Args:
request: Reference to the current request object.
Returns:
An :class:`oauth2.client.Storage` object.
"""
storage_model = oauth2_settings.storage_model
user_property = oauth2_settings.storage_model_user_property
credentials_property = oauth2_settings.storage_model_credentials_property
if storage_model:
module_name, class_name = storage_model.rsplit('.', 1)
module = importlib.import_module(module_name)
storage_model_class = getattr(module, class_name)
return storage.DjangoORMStorage(storage_model_class,
user_property,
request.user,
credentials_property)
else:
# use session
return dictionary_storage.DictionaryStorage(
request.session, key=_CREDENTIALS_KEY)
def _redirect_with_params(url_name, *args, **kwargs):
"""Helper method to create a redirect response with URL params.
This builds a redirect string that converts kwargs into a
query string.
Args:
url_name: The name of the url to redirect to.
kwargs: the query string param and their values to build.
Returns:
A properly formatted redirect string.
"""
url = urlresolvers.reverse(url_name, args=args)
params = parse.urlencode(kwargs, True)
return "{0}?{1}".format(url, params)
def _credentials_from_request(request):
"""Gets the authorized credentials for this flow, if they exist."""
# ORM storage requires a logged in user
if (oauth2_settings.storage_model is None or
request.user.is_authenticated()):
return get_storage(request).get()
else:
return None
class UserOAuth2(object):
"""Class to create oauth2 objects on Django request objects containing
credentials and helper methods.
"""
def __init__(self, request, scopes=None, return_url=None):
"""Initialize the Oauth2 Object.
Args:
request: Django request object.
scopes: Scopes desired for this OAuth2 flow.
return_url: The url to return to after the OAuth flow is complete,
defaults to the request's current URL path.
"""
self.request = request
self.return_url = return_url or request.get_full_path()
if scopes:
self._scopes = set(oauth2_settings.scopes) | set(scopes)
else:
self._scopes = set(oauth2_settings.scopes)
def get_authorize_redirect(self):
"""Creates a URl to start the OAuth2 authorization flow."""
get_params = {
'return_url': self.return_url,
'scopes': self._get_scopes()
}
return _redirect_with_params('google_oauth:authorize', **get_params)
def has_credentials(self):
"""Returns True if there are valid credentials for the current user
and required scopes."""
credentials = _credentials_from_request(self.request)
return (credentials and not credentials.invalid and
credentials.has_scopes(self._get_scopes()))
def _get_scopes(self):
"""Returns the scopes associated with this object, kept up to
date for incremental auth."""
if _credentials_from_request(self.request):
return (self._scopes |
_credentials_from_request(self.request).scopes)
else:
return self._scopes
@property
def scopes(self):
"""Returns the scopes associated with this OAuth2 object."""
# make sure previously requested custom scopes are maintained
# in future authorizations
return self._get_scopes()
@property
def credentials(self):
"""Gets the authorized credentials for this flow, if they exist."""
return _credentials_from_request(self.request)
@property
def http(self):
"""Helper: create HTTP client authorized with OAuth2 credentials."""
if self.has_credentials():
return self.credentials.authorize(transport.get_http_object())
return None

View File

@@ -1,32 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Application Config For Django OAuth2 Helper.
Django 1.7+ provides an
[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
API so that Django projects can introspect on installed applications using a
stable API. This module exists to follow that convention.
"""
import sys
# Django 1.7+ only supports Python 2.7+
if sys.hexversion >= 0x02070000: # pragma: NO COVER
from django.apps import AppConfig
class GoogleOAuth2HelperConfig(AppConfig):
""" App Config for Django Helper"""
name = 'oauth2client.django_util'
verbose_name = "Google OAuth2 Django Helper"

View File

@@ -1,145 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Decorators for Django OAuth2 Flow.
Contains two decorators, ``oauth_required`` and ``oauth_enabled``.
``oauth_required`` will ensure that a user has an oauth object containing
credentials associated with the request, and if not, redirect to the
authorization flow.
``oauth_enabled`` will attach the oauth2 object containing credentials if it
exists. If it doesn't, the view will still render, but helper methods will be
attached to start the oauth2 flow.
"""
from django import shortcuts
import django.conf
from six import wraps
from six.moves.urllib import parse
from oauth2client.contrib import django_util
def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
""" Decorator to require OAuth2 credentials for a view.
.. code-block:: python
:caption: views.py
:name: views_required_2
from oauth2client.django_util.decorators import oauth_required
@oauth_required
def requires_default_scopes(request):
email = request.credentials.id_token['email']
service = build(serviceName='calendar', version='v3',
http=request.oauth.http,
developerKey=API_KEY)
events = service.events().list(
calendarId='primary').execute()['items']
return HttpResponse(
"email: {0}, calendar: {1}".format(email, str(events)))
Args:
decorated_function: View function to decorate, must have the Django
request object as the first argument.
scopes: Scopes to require, will default.
decorator_kwargs: Can include ``return_url`` to specify the URL to
return to after OAuth2 authorization is complete.
Returns:
An OAuth2 Authorize view if credentials are not found or if the
credentials are missing the required scopes. Otherwise,
the decorated view.
"""
def curry_wrapper(wrapped_function):
@wraps(wrapped_function)
def required_wrapper(request, *args, **kwargs):
if not (django_util.oauth2_settings.storage_model is None or
request.user.is_authenticated()):
redirect_str = '{0}?next={1}'.format(
django.conf.settings.LOGIN_URL,
parse.quote(request.path))
return shortcuts.redirect(redirect_str)
return_url = decorator_kwargs.pop('return_url',
request.get_full_path())
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
if not user_oauth.has_credentials():
return shortcuts.redirect(user_oauth.get_authorize_redirect())
setattr(request, django_util.oauth2_settings.request_prefix,
user_oauth)
return wrapped_function(request, *args, **kwargs)
return required_wrapper
if decorated_function:
return curry_wrapper(decorated_function)
else:
return curry_wrapper
def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
""" Decorator to enable OAuth Credentials if authorized, and setup
the oauth object on the request object to provide helper functions
to start the flow otherwise.
.. code-block:: python
:caption: views.py
:name: views_enabled3
from oauth2client.django_util.decorators import oauth_enabled
@oauth_enabled
def optional_oauth2(request):
if request.oauth.has_credentials():
# this could be passed into a view
# request.oauth.http is also initialized
return HttpResponse("User email: {0}".format(
request.oauth.credentials.id_token['email'])
else:
return HttpResponse('Here is an OAuth Authorize link:
<a href="{0}">Authorize</a>'.format(
request.oauth.get_authorize_redirect()))
Args:
decorated_function: View function to decorate.
scopes: Scopes to require, will default.
decorator_kwargs: Can include ``return_url`` to specify the URL to
return to after OAuth2 authorization is complete.
Returns:
The decorated view function.
"""
def curry_wrapper(wrapped_function):
@wraps(wrapped_function)
def enabled_wrapper(request, *args, **kwargs):
return_url = decorator_kwargs.pop('return_url',
request.get_full_path())
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
setattr(request, django_util.oauth2_settings.request_prefix,
user_oauth)
return wrapped_function(request, *args, **kwargs)
return enabled_wrapper
if decorated_function:
return curry_wrapper(decorated_function)
else:
return curry_wrapper

View File

@@ -1,82 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains classes used for the Django ORM storage."""
import base64
import pickle
from django.db import models
from django.utils import encoding
import jsonpickle
import oauth2client
class CredentialsField(models.Field):
"""Django ORM field for storing OAuth2 Credentials."""
def __init__(self, *args, **kwargs):
if 'null' not in kwargs:
kwargs['null'] = True
super(CredentialsField, self).__init__(*args, **kwargs)
def get_internal_type(self):
return 'BinaryField'
def from_db_value(self, value, expression, connection, context):
"""Overrides ``models.Field`` method. This converts the value
returned from the database to an instance of this class.
"""
return self.to_python(value)
def to_python(self, value):
"""Overrides ``models.Field`` method. This is used to convert
bytes (from serialization etc) to an instance of this class"""
if value is None:
return None
elif isinstance(value, oauth2client.client.Credentials):
return value
else:
try:
return jsonpickle.decode(
base64.b64decode(encoding.smart_bytes(value)).decode())
except ValueError:
return pickle.loads(
base64.b64decode(encoding.smart_bytes(value)))
def get_prep_value(self, value):
"""Overrides ``models.Field`` method. This is used to convert
the value from an instances of this class to bytes that can be
inserted into the database.
"""
if value is None:
return None
else:
return encoding.smart_text(
base64.b64encode(jsonpickle.encode(value).encode()))
def value_to_string(self, obj):
"""Convert the field value from the provided model to a string.
Used during model serialization.
Args:
obj: db.Model, model object
Returns:
string, the serialized field value
"""
value = self._get_val_from_obj(obj)
return self.get_prep_value(value)

View File

@@ -1,28 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Signals for Google OAuth2 Helper.
This module contains signals for Google OAuth2 Helper. Currently it only
contains one, which fires when an OAuth2 authorization flow has completed.
"""
import django.dispatch
"""Signal that fires when OAuth2 Flow has completed.
It passes the Django request object and the OAuth2 credentials object to the
receiver.
"""
oauth2_authorized = django.dispatch.Signal(
providing_args=["request", "credentials"])

View File

@@ -1,26 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains Django URL patterns used for OAuth2 flow."""
from django.conf import urls
from oauth2client.contrib.django_util import views
urlpatterns = [
urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
]
urls = (urlpatterns, "google_oauth", "google_oauth")

View File

@@ -1,81 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains a storage module that stores credentials using the Django ORM."""
from oauth2client import client
class DjangoORMStorage(client.Storage):
"""Store and retrieve a single credential to and from the Django datastore.
This Storage helper presumes the Credentials
have been stored as a CredentialsField
on a db model class.
"""
def __init__(self, model_class, key_name, key_value, property_name):
"""Constructor for Storage.
Args:
model: string, fully qualified name of db.Model model class.
key_name: string, key name for the entity that has the credentials
key_value: string, key value for the entity that has the
credentials.
property_name: string, name of the property that is an
CredentialsProperty.
"""
super(DjangoORMStorage, self).__init__()
self.model_class = model_class
self.key_name = key_name
self.key_value = key_value
self.property_name = property_name
def locked_get(self):
"""Retrieve stored credential from the Django ORM.
Returns:
oauth2client.Credentials retrieved from the Django ORM, associated
with the ``model``, ``key_value``->``key_name`` pair used to query
for the model, and ``property_name`` identifying the
``CredentialsProperty`` field, all of which are defined in the
constructor for this Storage object.
"""
query = {self.key_name: self.key_value}
entities = self.model_class.objects.filter(**query)
if len(entities) > 0:
credential = getattr(entities[0], self.property_name)
if getattr(credential, 'set_store', None) is not None:
credential.set_store(self)
return credential
else:
return None
def locked_put(self, credentials):
"""Write a Credentials to the Django datastore.
Args:
credentials: Credentials, the credentials to store.
"""
entity, _ = self.model_class.objects.get_or_create(
**{self.key_name: self.key_value})
setattr(entity, self.property_name, credentials)
entity.save()
def locked_delete(self):
"""Delete Credentials from the datastore."""
query = {self.key_name: self.key_value}
self.model_class.objects.filter(**query).delete()

View File

@@ -1,193 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains the views used by the OAuth2 flows.
Their are two views used by the OAuth2 flow, the authorize and the callback
view. The authorize view kicks off the three-legged OAuth flow, and the
callback view validates the flow and if successful stores the credentials
in the configured storage."""
import hashlib
import json
import os
from django import http
from django import shortcuts
from django.conf import settings
from django.core import urlresolvers
from django.shortcuts import redirect
from django.utils import html
import jsonpickle
from six.moves.urllib import parse
from oauth2client import client
from oauth2client.contrib import django_util
from oauth2client.contrib.django_util import get_storage
from oauth2client.contrib.django_util import signals
_CSRF_KEY = 'google_oauth2_csrf_token'
_FLOW_KEY = 'google_oauth2_flow_{0}'
def _make_flow(request, scopes, return_url=None):
"""Creates a Web Server Flow
Args:
request: A Django request object.
scopes: the request oauth2 scopes.
return_url: The URL to return to after the flow is complete. Defaults
to the path of the current request.
Returns:
An OAuth2 flow object that has been stored in the session.
"""
# Generate a CSRF token to prevent malicious requests.
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
request.session[_CSRF_KEY] = csrf_token
state = json.dumps({
'csrf_token': csrf_token,
'return_url': return_url,
})
flow = client.OAuth2WebServerFlow(
client_id=django_util.oauth2_settings.client_id,
client_secret=django_util.oauth2_settings.client_secret,
scope=scopes,
state=state,
redirect_uri=request.build_absolute_uri(
urlresolvers.reverse("google_oauth:callback")))
flow_key = _FLOW_KEY.format(csrf_token)
request.session[flow_key] = jsonpickle.encode(flow)
return flow
def _get_flow_for_token(csrf_token, request):
""" Looks up the flow in session to recover information about requested
scopes.
Args:
csrf_token: The token passed in the callback request that should
match the one previously generated and stored in the request on the
initial authorization view.
Returns:
The OAuth2 Flow object associated with this flow based on the
CSRF token.
"""
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
return None if flow_pickle is None else jsonpickle.decode(flow_pickle)
def oauth2_callback(request):
""" View that handles the user's return from OAuth2 provider.
This view verifies the CSRF state and OAuth authorization code, and on
success stores the credentials obtained in the storage provider,
and redirects to the return_url specified in the authorize view and
stored in the session.
Args:
request: Django request.
Returns:
A redirect response back to the return_url.
"""
if 'error' in request.GET:
reason = request.GET.get(
'error_description', request.GET.get('error', ''))
reason = html.escape(reason)
return http.HttpResponseBadRequest(
'Authorization failed {0}'.format(reason))
try:
encoded_state = request.GET['state']
code = request.GET['code']
except KeyError:
return http.HttpResponseBadRequest(
'Request missing state or authorization code')
try:
server_csrf = request.session[_CSRF_KEY]
except KeyError:
return http.HttpResponseBadRequest(
'No existing session for this flow.')
try:
state = json.loads(encoded_state)
client_csrf = state['csrf_token']
return_url = state['return_url']
except (ValueError, KeyError):
return http.HttpResponseBadRequest('Invalid state parameter.')
if client_csrf != server_csrf:
return http.HttpResponseBadRequest('Invalid CSRF token.')
flow = _get_flow_for_token(client_csrf, request)
if not flow:
return http.HttpResponseBadRequest('Missing Oauth2 flow.')
try:
credentials = flow.step2_exchange(code)
except client.FlowExchangeError as exchange_error:
return http.HttpResponseBadRequest(
'An error has occurred: {0}'.format(exchange_error))
get_storage(request).put(credentials)
signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
request=request, credentials=credentials)
return shortcuts.redirect(return_url)
def oauth2_authorize(request):
""" View to start the OAuth2 Authorization flow.
This view starts the OAuth2 authorization flow. If scopes is passed in
as a GET URL parameter, it will authorize those scopes, otherwise the
default scopes specified in settings. The return_url can also be
specified as a GET parameter, otherwise the referer header will be
checked, and if that isn't found it will return to the root path.
Args:
request: The Django request object.
Returns:
A redirect to Google OAuth2 Authorization.
"""
return_url = request.GET.get('return_url', None)
if not return_url:
return_url = request.META.get('HTTP_REFERER', '/')
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
# 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
else:
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
if user_oauth.has_credentials():
return redirect(return_url)
flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
auth_url = flow.step1_get_authorize_url()
return shortcuts.redirect(auth_url)

View File

@@ -1,557 +0,0 @@
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for the Flask web framework
Provides a Flask extension that makes using OAuth2 web server flow easier.
The extension includes views that handle the entire auth flow and a
``@required`` decorator to automatically ensure that user credentials are
available.
Configuration
=============
To configure, you'll need a set of OAuth2 web application credentials from the
`Google Developer's Console <https://console.developers.google.com/project/_/\
apiui/credential>`__.
.. code-block:: python
from oauth2client.contrib.flask_util import UserOAuth2
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json'
# or, specify the client id and secret separately
app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
oauth2 = UserOAuth2(app)
Usage
=====
Once configured, you can use the :meth:`UserOAuth2.required` decorator to
ensure that credentials are available within a view.
.. code-block:: python
:emphasize-lines: 3,7,10
# Note that app.route should be the outermost decorator.
@app.route('/needs_credentials')
@oauth2.required
def example():
# http is authorized with the user's credentials and can be used
# to make http calls.
http = oauth2.http()
# Or, you can access the credentials directly
credentials = oauth2.credentials
If you want credentials to be optional for a view, you can leave the decorator
off and use :meth:`UserOAuth2.has_credentials` to check.
.. code-block:: python
:emphasize-lines: 3
@app.route('/optional')
def optional():
if oauth2.has_credentials():
return 'Credentials found!'
else:
return 'No credentials!'
When credentials are available, you can use :attr:`UserOAuth2.email` and
:attr:`UserOAuth2.user_id` to access information from the `ID Token
<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, if
available.
.. code-block:: python
:emphasize-lines: 4
@app.route('/info')
@oauth2.required
def info():
return "Hello, {} ({})".format(oauth2.email, oauth2.user_id)
URLs & Trigging Authorization
=============================
The extension will add two new routes to your application:
* ``"oauth2.authorize"`` -> ``/oauth2authorize``
* ``"oauth2.callback"`` -> ``/oauth2callback``
When configuring your OAuth2 credentials on the Google Developer's Console, be
sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized
callback url.
Typically you don't not need to use these routes directly, just be sure to
decorate any views that require credentials with ``@oauth2.required``. If
needed, you can trigger authorization at any time by redirecting the user
to the URL returned by :meth:`UserOAuth2.authorize_url`.
.. code-block:: python
:emphasize-lines: 3
@app.route('/login')
def login():
return oauth2.authorize_url("/")
Incremental Auth
================
This extension also supports `Incremental Auth <https://developers.google.com\
/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it,
configure the extension with ``include_granted_scopes``.
.. code-block:: python
oauth2 = UserOAuth2(app, include_granted_scopes=True)
Then specify any additional scopes needed on the decorator, for example:
.. code-block:: python
:emphasize-lines: 2,7
@app.route('/drive')
@oauth2.required(scopes=["https://www.googleapis.com/auth/drive"])
def requires_drive():
...
@app.route('/calendar')
@oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"])
def requires_calendar():
...
The decorator will ensure that the the user has authorized all specified scopes
before allowing them to access the view, and will also ensure that credentials
do not lose any previously authorized scopes.
Storage
=======
By default, the extension uses a Flask session-based storage solution. This
means that credentials are only available for the duration of a session. It
also means that with Flask's default configuration, the credentials will be
visible in the session cookie. It's highly recommended to use database-backed
session and to use https whenever handling user credentials.
If you need the credentials to be available longer than a user session or
available outside of a request context, you will need to implement your own
:class:`oauth2client.Storage`.
"""
from functools import wraps
import hashlib
import json
import os
import pickle
try:
from flask import Blueprint
from flask import _app_ctx_stack
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
import markupsafe
except ImportError: # pragma: NO COVER
raise ImportError('The flask utilities require flask 0.9 or newer.')
import six.moves.http_client as httplib
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client.contrib import dictionary_storage
_DEFAULT_SCOPES = ('email',)
_CREDENTIALS_KEY = 'google_oauth2_credentials'
_FLOW_KEY = 'google_oauth2_flow_{0}'
_CSRF_KEY = 'google_oauth2_csrf_token'
def _get_flow_for_token(csrf_token):
"""Retrieves the flow instance associated with a given CSRF token from
the Flask session."""
flow_pickle = session.pop(
_FLOW_KEY.format(csrf_token), None)
if flow_pickle is None:
return None
else:
return pickle.loads(flow_pickle)
class UserOAuth2(object):
"""Flask extension for making OAuth 2.0 easier.
Configuration values:
* ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json
file, obtained from the credentials screen in the Google Developers
console.
* ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not
specified.
* ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE``
is not specified.
If app is specified, all arguments will be passed along to init_app.
If no app is specified, then you should call init_app in your application
factory to finish initialization.
"""
def __init__(self, app=None, *args, **kwargs):
self.app = app
if app is not None:
self.init_app(app, *args, **kwargs)
def init_app(self, app, scopes=None, client_secrets_file=None,
client_id=None, client_secret=None, authorize_callback=None,
storage=None, **kwargs):
"""Initialize this extension for the given app.
Arguments:
app: A Flask application.
scopes: Optional list of scopes to authorize.
client_secrets_file: Path to a file containing client secrets. You
can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config
value.
client_id: If not specifying a client secrets file, specify the
OAuth2 client id. You can also specify the
GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
client secret.
client_secret: The OAuth2 client secret. You can also specify the
GOOGLE_OAUTH2_CLIENT_SECRET config value.
authorize_callback: A function that is executed after successful
user authorization.
storage: A oauth2client.client.Storage subclass for storing the
credentials. By default, this is a Flask session based storage.
kwargs: Any additional args are passed along to the Flow
constructor.
"""
self.app = app
self.authorize_callback = authorize_callback
self.flow_kwargs = kwargs
if storage is None:
storage = dictionary_storage.DictionaryStorage(
session, key=_CREDENTIALS_KEY)
self.storage = storage
if scopes is None:
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
self.scopes = scopes
self._load_config(client_secrets_file, client_id, client_secret)
app.register_blueprint(self._create_blueprint())
def _load_config(self, client_secrets_file, client_id, client_secret):
"""Loads oauth2 configuration in order of priority.
Priority:
1. Config passed to the constructor or init_app.
2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
config.
3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
GOOGLE_OAUTH2_CLIENT_SECRET app config.
Raises:
ValueError if no config could be found.
"""
if client_id and client_secret:
self.client_id, self.client_secret = client_id, client_secret
return
if client_secrets_file:
self._load_client_secrets(client_secrets_file)
return
if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
self._load_client_secrets(
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
return
try:
self.client_id, self.client_secret = (
self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
except KeyError:
raise ValueError(
'OAuth2 configuration could not be found. Either specify the '
'client_secrets_file or client_id and client_secret or set '
'the app configuration variables '
'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
def _load_client_secrets(self, filename):
"""Loads client secrets from the given filename."""
client_type, client_info = clientsecrets.loadfile(filename)
if client_type != clientsecrets.TYPE_WEB:
raise ValueError(
'The flow specified in {0} is not supported.'.format(
client_type))
self.client_id = client_info['client_id']
self.client_secret = client_info['client_secret']
def _make_flow(self, return_url=None, **kwargs):
"""Creates a Web Server Flow"""
# Generate a CSRF token to prevent malicious requests.
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
session[_CSRF_KEY] = csrf_token
state = json.dumps({
'csrf_token': csrf_token,
'return_url': return_url
})
kw = self.flow_kwargs.copy()
kw.update(kwargs)
extra_scopes = kw.pop('scopes', [])
scopes = set(self.scopes).union(set(extra_scopes))
flow = client.OAuth2WebServerFlow(
client_id=self.client_id,
client_secret=self.client_secret,
scope=scopes,
state=state,
redirect_uri=url_for('oauth2.callback', _external=True),
**kw)
flow_key = _FLOW_KEY.format(csrf_token)
session[flow_key] = pickle.dumps(flow)
return flow
def _create_blueprint(self):
bp = Blueprint('oauth2', __name__)
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
return bp
def authorize_view(self):
"""Flask view that starts the authorization flow.
Starts flow by redirecting the user to the OAuth2 provider.
"""
args = request.args.to_dict()
# Scopes will be passed as mutliple args, and to_dict() will only
# return one. So, we use getlist() to get all of the scopes.
args['scopes'] = request.args.getlist('scopes')
return_url = args.pop('return_url', None)
if return_url is None:
return_url = request.referrer or '/'
flow = self._make_flow(return_url=return_url, **args)
auth_url = flow.step1_get_authorize_url()
return redirect(auth_url)
def callback_view(self):
"""Flask view that handles the user's return from OAuth2 provider.
On return, exchanges the authorization code for credentials and stores
the credentials.
"""
if 'error' in request.args:
reason = request.args.get(
'error_description', request.args.get('error', ''))
reason = markupsafe.escape(reason)
return ('Authorization failed: {0}'.format(reason),
httplib.BAD_REQUEST)
try:
encoded_state = request.args['state']
server_csrf = session[_CSRF_KEY]
code = request.args['code']
except KeyError:
return 'Invalid request', httplib.BAD_REQUEST
try:
state = json.loads(encoded_state)
client_csrf = state['csrf_token']
return_url = state['return_url']
except (ValueError, KeyError):
return 'Invalid request state', httplib.BAD_REQUEST
if client_csrf != server_csrf:
return 'Invalid request state', httplib.BAD_REQUEST
flow = _get_flow_for_token(server_csrf)
if flow is None:
return 'Invalid request state', httplib.BAD_REQUEST
# Exchange the auth code for credentials.
try:
credentials = flow.step2_exchange(code)
except client.FlowExchangeError as exchange_error:
current_app.logger.exception(exchange_error)
content = 'An error occurred: {0}'.format(exchange_error)
return content, httplib.BAD_REQUEST
# Save the credentials to the storage.
self.storage.put(credentials)
if self.authorize_callback:
self.authorize_callback(credentials)
return redirect(return_url)
@property
def credentials(self):
"""The credentials for the current user or None if unavailable."""
ctx = _app_ctx_stack.top
if not hasattr(ctx, _CREDENTIALS_KEY):
ctx.google_oauth2_credentials = self.storage.get()
return ctx.google_oauth2_credentials
def has_credentials(self):
"""Returns True if there are valid credentials for the current user."""
if not self.credentials:
return False
# Is the access token expired? If so, do we have an refresh token?
elif (self.credentials.access_token_expired and
not self.credentials.refresh_token):
return False
else:
return True
@property
def email(self):
"""Returns the user's email address or None if there are no credentials.
The email address is provided by the current credentials' id_token.
This should not be used as unique identifier as the user can change
their email. If you need a unique identifier, use user_id.
"""
if not self.credentials:
return None
try:
return self.credentials.id_token['email']
except KeyError:
current_app.logger.error(
'Invalid id_token {0}'.format(self.credentials.id_token))
@property
def user_id(self):
"""Returns the a unique identifier for the user
Returns None if there are no credentials.
The id is provided by the current credentials' id_token.
"""
if not self.credentials:
return None
try:
return self.credentials.id_token['sub']
except KeyError:
current_app.logger.error(
'Invalid id_token {0}'.format(self.credentials.id_token))
def authorize_url(self, return_url, **kwargs):
"""Creates a URL that can be used to start the authorization flow.
When the user is directed to the URL, the authorization flow will
begin. Once complete, the user will be redirected to the specified
return URL.
Any kwargs are passed into the flow constructor.
"""
return url_for('oauth2.authorize', return_url=return_url, **kwargs)
def required(self, decorated_function=None, scopes=None,
**decorator_kwargs):
"""Decorator to require OAuth2 credentials for a view.
If credentials are not available for the current user, then they will
be redirected to the authorization flow. Once complete, the user will
be redirected back to the original page.
"""
def curry_wrapper(wrapped_function):
@wraps(wrapped_function)
def required_wrapper(*args, **kwargs):
return_url = decorator_kwargs.pop('return_url', request.url)
requested_scopes = set(self.scopes)
if scopes is not None:
requested_scopes |= set(scopes)
if self.has_credentials():
requested_scopes |= self.credentials.scopes
requested_scopes = list(requested_scopes)
# Does the user have credentials and does the credentials have
# all of the needed scopes?
if (self.has_credentials() and
self.credentials.has_scopes(requested_scopes)):
return wrapped_function(*args, **kwargs)
# Otherwise, redirect to authorization
else:
auth_url = self.authorize_url(
return_url,
scopes=requested_scopes,
**decorator_kwargs)
return redirect(auth_url)
return required_wrapper
if decorated_function:
return curry_wrapper(decorated_function)
else:
return curry_wrapper
def http(self, *args, **kwargs):
"""Returns an authorized http instance.
Can only be called if there are valid credentials for the user, such
as inside of a view that is decorated with @required.
Args:
*args: Positional arguments passed to httplib2.Http constructor.
**kwargs: Positional arguments passed to httplib2.Http constructor.
Raises:
ValueError if no credentials are available.
"""
if not self.credentials:
raise ValueError('No credentials available.')
return self.credentials.authorize(
transport.get_http_object(*args, **kwargs))

View File

@@ -1,156 +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.
"""Utilities for Google Compute Engine
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
"""
import logging
import warnings
from six.moves import http_client
from oauth2client import client
from oauth2client.contrib import _metadata
logger = logging.getLogger(__name__)
_SCOPES_WARNING = """\
You have requested explicit scopes to be used with a GCE service account.
Using this argument will have no effect on the actual scopes for tokens
requested. These scopes are set at VM instance creation time and
can't be overridden in the request.
"""
class AppAssertionCredentials(client.AssertionCredentials):
"""Credentials object for Compute Engine Assertion Grants
This object will allow a Compute Engine instance to identify itself to
Google and other OAuth 2.0 servers that can verify assertions. It can be
used for the purpose of accessing data stored under an account assigned to
the Compute Engine instance itself.
This credential does not require a flow to instantiate because it
represents a two legged flow, and therefore has all of the required
information to generate and refresh its own access tokens.
Note that :attr:`service_account_email` and :attr:`scopes`
will both return None until the credentials have been refreshed.
To check whether credentials have previously been refreshed use
:attr:`invalid`.
"""
def __init__(self, email=None, *args, **kwargs):
"""Constructor for AppAssertionCredentials
Args:
email: an email that specifies the service account to use.
Only necessary if using custom service accounts
(see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount).
"""
if 'scopes' in kwargs:
warnings.warn(_SCOPES_WARNING)
kwargs['scopes'] = None
# Assertion type is no longer used, but still in the
# parent class signature.
super(AppAssertionCredentials, self).__init__(None, *args, **kwargs)
self.service_account_email = email
self.scopes = None
self.invalid = True
@classmethod
def from_json(cls, json_data):
raise NotImplementedError(
'Cannot serialize credentials for GCE service accounts.')
def to_json(self):
raise NotImplementedError(
'Cannot serialize credentials for GCE service accounts.')
def retrieve_scopes(self, http):
"""Retrieves the canonical list of scopes for this access token.
Overrides client.Credentials.retrieve_scopes. Fetches scopes info
from the metadata server.
Args:
http: httplib2.Http, an http object to be used to make the refresh
request.
Returns:
A set of strings containing the canonical list of scopes.
"""
self._retrieve_info(http)
return self.scopes
def _retrieve_info(self, http):
"""Retrieves service account info for invalid credentials.
Args:
http: an object to be used to make HTTP requests.
"""
if self.invalid:
info = _metadata.get_service_account_info(
http,
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):
"""Refreshes the access token.
Skip all the storage hoops and just refresh using the API.
Args:
http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
"""
try:
self._retrieve_info(http)
self.access_token, self.token_expiry = _metadata.get_token(
http, service_account=self.service_account_email)
except http_client.HTTPException as err:
raise client.HttpAccessTokenRefreshError(str(err))
@property
def serialization_data(self):
raise NotImplementedError(
'Cannot serialize credentials for GCE service accounts.')
def create_scoped_required(self):
return False
def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
This method is provided to support a common interface, but
the actual key used for a Google Compute Engine service account
is not available, so it can't be used to sign content.
Args:
blob: bytes, Message to be signed.
Raises:
NotImplementedError, always.
"""
raise NotImplementedError(
'Compute Engine service accounts cannot sign blobs')

View File

@@ -1,95 +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.
"""A keyring based Storage.
A Storage for Credentials that uses the keyring module.
"""
import threading
import keyring
from oauth2client import client
class Storage(client.Storage):
"""Store and retrieve a single credential to and from the keyring.
To use this module you must have the keyring module installed. See
<http://pypi.python.org/pypi/keyring/>. This is an optional module and is
not installed with oauth2client by default because it does not work on all
the platforms that oauth2client supports, such as Google App Engine.
The keyring module <http://pypi.python.org/pypi/keyring/> is a
cross-platform library for access the keyring capabilities of the local
system. The user will be prompted for their keyring password when this
module is used, and the manner in which the user is prompted will vary per
platform.
Usage::
from oauth2client import keyring_storage
s = keyring_storage.Storage('name_of_application', 'user1')
credentials = s.get()
"""
def __init__(self, service_name, user_name):
"""Constructor.
Args:
service_name: string, The name of the service under which the
credentials are stored.
user_name: string, The name of the user to store credentials for.
"""
super(Storage, self).__init__(lock=threading.Lock())
self._service_name = service_name
self._user_name = user_name
def locked_get(self):
"""Retrieve Credential from file.
Returns:
oauth2client.client.Credentials
"""
credentials = None
content = keyring.get_password(self._service_name, self._user_name)
if content is not None:
try:
credentials = client.Credentials.new_from_json(content)
credentials.set_store(self)
except ValueError:
pass
return credentials
def locked_put(self, credentials):
"""Write Credentials to file.
Args:
credentials: Credentials, the credentials to store.
"""
keyring.set_password(self._service_name, self._user_name,
credentials.to_json())
def locked_delete(self):
"""Delete Credentials file.
Args:
credentials: Credentials, the credentials to store.
"""
keyring.set_password(self._service_name, self._user_name, '')

View File

@@ -1,355 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Multiprocess file credential storage.
This module provides file-based storage that supports multiple credentials and
cross-thread and process access.
This module supersedes the functionality previously found in `multistore_file`.
This module provides :class:`MultiprocessFileStorage` which:
* Is tied to a single credential via a user-specified key. This key can be
used to distinguish between multiple users, client ids, and/or scopes.
* Can be safely accessed and refreshed across threads and processes.
Process & thread safety guarantees the following behavior:
* If one thread or process refreshes a credential, subsequent refreshes
from other processes will re-fetch the credentials from the file instead
of performing an http request.
* If two processes or threads attempt to refresh concurrently, only one
will be able to acquire the lock and refresh, with the deadlock caveat
below.
* The interprocess lock will not deadlock, instead, the if a process can
not acquire the interprocess lock within ``INTERPROCESS_LOCK_DEADLINE``
it will allow refreshing the credential but will not write the updated
credential to disk, This logic happens during every lock cycle - if the
credentials are refreshed again it will retry locking and writing as
normal.
Usage
=====
Before using the storage, you need to decide how you want to key the
credentials. A few common strategies include:
* If you're storing credentials for multiple users in a single file, use
a unique identifier for each user as the key.
* If you're storing credentials for multiple client IDs in a single file,
use the client ID as the key.
* If you're storing multiple credentials for one user, use the scopes as
the key.
* If you have a complicated setup, use a compound key. For example, you
can use a combination of the client ID and scopes as the key.
Create an instance of :class:`MultiprocessFileStorage` for each credential you
want to store, for example::
filename = 'credentials'
key = '{}-{}'.format(client_id, user_id)
storage = MultiprocessFileStorage(filename, key)
To store the credentials::
storage.put(credentials)
If you're going to continue to use the credentials after storing them, be sure
to call :func:`set_store`::
credentials.set_store(storage)
To retrieve the credentials::
storage.get(credentials)
"""
import base64
import json
import logging
import os
import threading
import fasteners
from six import iteritems
from oauth2client import _helpers
from oauth2client import client
#: The maximum amount of time, in seconds, to wait when acquire the
#: interprocess lock before falling back to read-only mode.
INTERPROCESS_LOCK_DEADLINE = 1
logger = logging.getLogger(__name__)
_backends = {}
_backends_lock = threading.Lock()
def _create_file_if_needed(filename):
"""Creates the an empty file if it does not already exist.
Returns:
True if the file was created, False otherwise.
"""
if os.path.exists(filename):
return False
else:
# Equivalent to "touch".
open(filename, 'a+b').close()
logger.info('Credential file {0} created'.format(filename))
return True
def _load_credentials_file(credentials_file):
"""Load credentials from the given file handle.
The file is expected to be in this format:
{
"file_version": 2,
"credentials": {
"key": "base64 encoded json representation of credentials."
}
}
This function will warn and return empty credentials instead of raising
exceptions.
Args:
credentials_file: An open file handle.
Returns:
A dictionary mapping user-defined keys to an instance of
:class:`oauth2client.client.Credentials`.
"""
try:
credentials_file.seek(0)
data = json.load(credentials_file)
except Exception:
logger.warning(
'Credentials file could not be loaded, will ignore and '
'overwrite.')
return {}
if data.get('file_version') != 2:
logger.warning(
'Credentials file is not version 2, will ignore and '
'overwrite.')
return {}
credentials = {}
for key, encoded_credential in iteritems(data.get('credentials', {})):
try:
credential_json = base64.b64decode(encoded_credential)
credential = client.Credentials.new_from_json(credential_json)
credentials[key] = credential
except:
logger.warning(
'Invalid credential {0} in file, ignoring.'.format(key))
return credentials
def _write_credentials_file(credentials_file, credentials):
"""Writes credentials to a file.
Refer to :func:`_load_credentials_file` for the format.
Args:
credentials_file: An open file handle, must be read/write.
credentials: A dictionary mapping user-defined keys to an instance of
:class:`oauth2client.client.Credentials`.
"""
data = {'file_version': 2, 'credentials': {}}
for key, credential in iteritems(credentials):
credential_json = credential.to_json()
encoded_credential = _helpers._from_bytes(base64.b64encode(
_helpers._to_bytes(credential_json)))
data['credentials'][key] = encoded_credential
credentials_file.seek(0)
json.dump(data, credentials_file)
credentials_file.truncate()
class _MultiprocessStorageBackend(object):
"""Thread-local backend for multiprocess storage.
Each process has only one instance of this backend per file. All threads
share a single instance of this backend. This ensures that all threads
use the same thread lock and process lock when accessing the file.
"""
def __init__(self, filename):
self._file = None
self._filename = filename
self._process_lock = fasteners.InterProcessLock(
'{0}.lock'.format(filename))
self._thread_lock = threading.Lock()
self._read_only = False
self._credentials = {}
def _load_credentials(self):
"""(Re-)loads the credentials from the file."""
if not self._file:
return
loaded_credentials = _load_credentials_file(self._file)
self._credentials.update(loaded_credentials)
logger.debug('Read credential file')
def _write_credentials(self):
if self._read_only:
logger.debug('In read-only mode, not writing credentials.')
return
_write_credentials_file(self._file, self._credentials)
logger.debug('Wrote credential file {0}.'.format(self._filename))
def acquire_lock(self):
self._thread_lock.acquire()
locked = self._process_lock.acquire(timeout=INTERPROCESS_LOCK_DEADLINE)
if locked:
_create_file_if_needed(self._filename)
self._file = open(self._filename, 'r+')
self._read_only = False
else:
logger.warn(
'Failed to obtain interprocess lock for credentials. '
'If a credential is being refreshed, other processes may '
'not see the updated access token and refresh as well.')
if os.path.exists(self._filename):
self._file = open(self._filename, 'r')
else:
self._file = None
self._read_only = True
self._load_credentials()
def release_lock(self):
if self._file is not None:
self._file.close()
self._file = None
if not self._read_only:
self._process_lock.release()
self._thread_lock.release()
def _refresh_predicate(self, credentials):
if credentials is None:
return True
elif credentials.invalid:
return True
elif credentials.access_token_expired:
return True
else:
return False
def locked_get(self, key):
# Check if the credential is already in memory.
credentials = self._credentials.get(key, None)
# Use the refresh predicate to determine if the entire store should be
# reloaded. This basically checks if the credentials are invalid
# or expired. This covers the situation where another process has
# refreshed the credentials and this process doesn't know about it yet.
# In that case, this process won't needlessly refresh the credentials.
if self._refresh_predicate(credentials):
self._load_credentials()
credentials = self._credentials.get(key, None)
return credentials
def locked_put(self, key, credentials):
self._load_credentials()
self._credentials[key] = credentials
self._write_credentials()
def locked_delete(self, key):
self._load_credentials()
self._credentials.pop(key, None)
self._write_credentials()
def _get_backend(filename):
"""A helper method to get or create a backend with thread locking.
This ensures that only one backend is used per-file per-process, so that
thread and process locks are appropriately shared.
Args:
filename: The full path to the credential storage file.
Returns:
An instance of :class:`_MultiprocessStorageBackend`.
"""
filename = os.path.abspath(filename)
with _backends_lock:
if filename not in _backends:
_backends[filename] = _MultiprocessStorageBackend(filename)
return _backends[filename]
class MultiprocessFileStorage(client.Storage):
"""Multiprocess file credential storage.
Args:
filename: The path to the file where credentials will be stored.
key: An arbitrary string used to uniquely identify this set of
credentials. For example, you may use the user's ID as the key or
a combination of the client ID and user ID.
"""
def __init__(self, filename, key):
self._key = key
self._backend = _get_backend(filename)
def acquire_lock(self):
self._backend.acquire_lock()
def release_lock(self):
self._backend.release_lock()
def locked_get(self):
"""Retrieves the current credentials from the store.
Returns:
An instance of :class:`oauth2client.client.Credentials` or `None`.
"""
credential = self._backend.locked_get(self._key)
if credential is not None:
credential.set_store(self)
return credential
def locked_put(self, credentials):
"""Writes the given credentials to the store.
Args:
credentials: an instance of
:class:`oauth2client.client.Credentials`.
"""
return self._backend.locked_put(self._key, credentials)
def locked_delete(self):
"""Deletes the current credentials from the store."""
return self._backend.locked_delete(self._key)

View File

@@ -1,173 +0,0 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""OAuth 2.0 utilities for SQLAlchemy.
Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy.
Configuration
=============
In order to use this storage, you'll need to create table
with :class:`oauth2client.contrib.sqlalchemy.CredentialsType` column.
It's recommended to either put this column on some sort of user info
table or put the column in a table with a belongs-to relationship to
a user info table.
Here's an example of a simple table with a :class:`CredentialsType`
column that's related to a user table by the `user_id` key.
.. code-block:: python
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from oauth2client.contrib.sqlalchemy import CredentialsType
Base = declarative_base()
class Credentials(Base):
__tablename__ = 'credentials'
user_id = Column(Integer, ForeignKey('user.id'))
credentials = Column(CredentialsType)
class User(Base):
id = Column(Integer, primary_key=True)
# bunch of other columns
credentials = relationship('Credentials')
Usage
=====
With tables ready, you are now able to store credentials in database.
We will reuse tables defined above.
.. code-block:: python
from sqlalchemy.orm import Session
from oauth2client.client import OAuth2Credentials
from oauth2client.contrib.sql_alchemy import Storage
session = Session()
user = session.query(User).first()
storage = Storage(
session=session,
model_class=Credentials,
# This is the key column used to identify
# the row that stores the credentials.
key_name='user_id',
key_value=user.id,
property_name='credentials',
)
# Store
credentials = OAuth2Credentials(...)
storage.put(credentials)
# Retrieve
credentials = storage.get()
# Delete
storage.delete()
"""
from __future__ import absolute_import
import sqlalchemy.types
from oauth2client import client
class CredentialsType(sqlalchemy.types.PickleType):
"""Type representing credentials.
Alias for :class:`sqlalchemy.types.PickleType`.
"""
class Storage(client.Storage):
"""Store and retrieve a single credential to and from SQLAlchemy.
This helper presumes the Credentials
have been stored as a Credentials column
on a db model class.
"""
def __init__(self, session, model_class, key_name,
key_value, property_name):
"""Constructor for Storage.
Args:
session: An instance of :class:`sqlalchemy.orm.Session`.
model_class: SQLAlchemy declarative mapping.
key_name: string, key name for the entity that has the credentials
key_value: key value for the entity that has the credentials
property_name: A string indicating which property on the
``model_class`` to store the credentials.
This property must be a
:class:`CredentialsType` column.
"""
super(Storage, self).__init__()
self.session = session
self.model_class = model_class
self.key_name = key_name
self.key_value = key_value
self.property_name = property_name
def locked_get(self):
"""Retrieve stored credential.
Returns:
A :class:`oauth2client.Credentials` instance or `None`.
"""
filters = {self.key_name: self.key_value}
query = self.session.query(self.model_class).filter_by(**filters)
entity = query.first()
if entity:
credential = getattr(entity, self.property_name)
if credential and hasattr(credential, 'set_store'):
credential.set_store(self)
return credential
else:
return None
def locked_put(self, credentials):
"""Write a credentials to the SQLAlchemy datastore.
Args:
credentials: :class:`oauth2client.Credentials`
"""
filters = {self.key_name: self.key_value}
query = self.session.query(self.model_class).filter_by(**filters)
entity = query.first()
if not entity:
entity = self.model_class(**filters)
setattr(entity, self.property_name, credentials)
self.session.add(entity)
def locked_delete(self):
"""Delete credentials from the SQLAlchemy datastore."""
filters = {self.key_name: self.key_value}
self.session.query(self.model_class).filter_by(**filters).delete()

View File

@@ -1,101 +0,0 @@
# Copyright 2014 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper methods for creating & verifying XSRF tokens."""
import base64
import binascii
import hmac
import time
from oauth2client import _helpers
# Delimiter character
DELIMITER = b':'
# 1 hour in seconds
DEFAULT_TIMEOUT_SECS = 60 * 60
@_helpers.positional(2)
def generate_token(key, user_id, action_id='', when=None):
"""Generates a URL-safe token for the given user, action, time tuple.
Args:
key: secret key to use.
user_id: the user ID of the authenticated user.
action_id: a string identifier of the action they requested
authorization for.
when: the time in seconds since the epoch at which the user was
authorized for this action. If not set the current time is used.
Returns:
A string XSRF protection token.
"""
digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8'))
digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8'))
digester.update(DELIMITER)
digester.update(_helpers._to_bytes(action_id, encoding='utf-8'))
digester.update(DELIMITER)
when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8')
digester.update(when)
digest = digester.digest()
token = base64.urlsafe_b64encode(digest + DELIMITER + when)
return token
@_helpers.positional(3)
def validate_token(key, token, user_id, action_id="", current_time=None):
"""Validates that the given token authorizes the user for the action.
Tokens are invalid if the time of issue is too old or if the token
does not match what generateToken outputs (i.e. the token was forged).
Args:
key: secret key to use.
token: a string of the token generated by generateToken.
user_id: the user ID of the authenticated user.
action_id: a string identifier of the action they requested
authorization for.
Returns:
A boolean - True if the user is authorized for the action, False
otherwise.
"""
if not token:
return False
try:
decoded = base64.urlsafe_b64decode(token)
token_time = int(decoded.split(DELIMITER)[-1])
except (TypeError, ValueError, binascii.Error):
return False
if current_time is None:
current_time = time.time()
# If the token is too old it's not valid.
if current_time - token_time > DEFAULT_TIMEOUT_SECS:
return False
# The given token should match the generated one with the same time.
expected_token = generate_token(key, user_id, action_id=action_id,
when=token_time)
if len(token) != len(expected_token):
return False
# Perform constant time comparison to avoid timing attacks
different = 0
for x, y in zip(bytearray(token), bytearray(expected_token)):
different |= x ^ y
return not different

View File

@@ -1,250 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Crypto-related routines for oauth2client."""
import json
import logging
import time
from oauth2client import _helpers
from oauth2client import _pure_python_crypt
RsaSigner = _pure_python_crypt.RsaSigner
RsaVerifier = _pure_python_crypt.RsaVerifier
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
logger = logging.getLogger(__name__)
class AppIdentityError(Exception):
"""Error to indicate crypto failure."""
def _bad_pkcs12_key_as_pem(*args, **kwargs):
raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
try:
from oauth2client import _openssl_crypt
OpenSSLSigner = _openssl_crypt.OpenSSLSigner
OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
except ImportError: # pragma: NO COVER
OpenSSLVerifier = None
OpenSSLSigner = None
pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
try:
from oauth2client import _pycrypto_crypt
PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
except ImportError: # pragma: NO COVER
PyCryptoVerifier = None
PyCryptoSigner = None
if OpenSSLSigner:
Signer = OpenSSLSigner
Verifier = OpenSSLVerifier
elif PyCryptoSigner: # pragma: NO COVER
Signer = PyCryptoSigner
Verifier = PyCryptoVerifier
else: # pragma: NO COVER
Signer = RsaSigner
Verifier = RsaVerifier
def make_signed_jwt(signer, payload, key_id=None):
"""Make a signed JWT.
See http://self-issued.info/docs/draft-jones-json-web-token.html.
Args:
signer: crypt.Signer, Cryptographic signer.
payload: dict, Dictionary of data to convert to JSON and then sign.
key_id: string, (Optional) Key ID header.
Returns:
string, The JWT for the payload.
"""
header = {'typ': 'JWT', 'alg': 'RS256'}
if key_id is not None:
header['kid'] = key_id
segments = [
_helpers._urlsafe_b64encode(_helpers._json_encode(header)),
_helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
]
signing_input = b'.'.join(segments)
signature = signer.sign(signing_input)
segments.append(_helpers._urlsafe_b64encode(signature))
logger.debug(str(segments))
return b'.'.join(segments)
def _verify_signature(message, signature, certs):
"""Verifies signed content using a list of certificates.
Args:
message: string or bytes, The message to verify.
signature: string or bytes, The signature on the message.
certs: iterable, certificates in PEM format.
Raises:
AppIdentityError: If none of the certificates can verify the message
against the signature.
"""
for pem in certs:
verifier = Verifier.from_string(pem, is_x509_cert=True)
if verifier.verify(message, signature):
return
# If we have not returned, no certificate confirms the signature.
raise AppIdentityError('Invalid token signature')
def _check_audience(payload_dict, audience):
"""Checks audience field from a JWT payload.
Does nothing if the passed in ``audience`` is null.
Args:
payload_dict: dict, A dictionary containing a JWT payload.
audience: string or NoneType, an audience to check for in
the JWT payload.
Raises:
AppIdentityError: If there is no ``'aud'`` field in the payload
dictionary but there is an ``audience`` to check.
AppIdentityError: If the ``'aud'`` field in the payload dictionary
does not match the ``audience``.
"""
if audience is None:
return
audience_in_payload = payload_dict.get('aud')
if audience_in_payload is None:
raise AppIdentityError(
'No aud field in token: {0}'.format(payload_dict))
if audience_in_payload != audience:
raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
audience_in_payload, audience, payload_dict))
def _verify_time_range(payload_dict):
"""Verifies the issued at and expiration from a JWT payload.
Makes sure the current time (in UTC) falls between the issued at and
expiration for the JWT (with some skew allowed for via
``CLOCK_SKEW_SECS``).
Args:
payload_dict: dict, A dictionary containing a JWT payload.
Raises:
AppIdentityError: If there is no ``'iat'`` field in the payload
dictionary.
AppIdentityError: If there is no ``'exp'`` field in the payload
dictionary.
AppIdentityError: If the JWT expiration is too far in the future (i.e.
if the expiration would imply a token lifetime
longer than what is allowed.)
AppIdentityError: If the token appears to have been issued in the
future (up to clock skew).
AppIdentityError: If the token appears to have expired in the past
(up to clock skew).
"""
# Get the current time to use throughout.
now = int(time.time())
# Make sure issued at and expiration are in the payload.
issued_at = payload_dict.get('iat')
if issued_at is None:
raise AppIdentityError(
'No iat field in token: {0}'.format(payload_dict))
expiration = payload_dict.get('exp')
if expiration is None:
raise AppIdentityError(
'No exp field in token: {0}'.format(payload_dict))
# Make sure the expiration gives an acceptable token lifetime.
if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
raise AppIdentityError(
'exp field too far in future: {0}'.format(payload_dict))
# Make sure (up to clock skew) that the token wasn't issued in the future.
earliest = issued_at - CLOCK_SKEW_SECS
if now < earliest:
raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
now, earliest, payload_dict))
# Make sure (up to clock skew) that the token isn't already expired.
latest = expiration + CLOCK_SKEW_SECS
if now > latest:
raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
now, latest, payload_dict))
def verify_signed_jwt_with_certs(jwt, certs, audience=None):
"""Verify a JWT against public certs.
See http://self-issued.info/docs/draft-jones-json-web-token.html.
Args:
jwt: string, A JWT.
certs: dict, Dictionary where values of public keys in PEM format.
audience: string, The audience, 'aud', that this JWT should contain. If
None then the JWT's 'aud' parameter is not verified.
Returns:
dict, The deserialized JSON payload in the JWT.
Raises:
AppIdentityError: if any checks are failed.
"""
jwt = _helpers._to_bytes(jwt)
if jwt.count(b'.') != 2:
raise AppIdentityError(
'Wrong number of segments in token: {0}'.format(jwt))
header, payload, signature = jwt.split(b'.')
message_to_sign = header + b'.' + payload
signature = _helpers._urlsafe_b64decode(signature)
# Parse token.
payload_bytes = _helpers._urlsafe_b64decode(payload)
try:
payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
except:
raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
# Verify that the signature matches the message.
_verify_signature(message_to_sign, signature, certs.values())
# Verify the issued at and created times in the payload.
_verify_time_range(payload_dict)
# Check audience.
_check_audience(payload_dict, audience)
return payload_dict

View File

@@ -1,95 +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.
"""Utilities for OAuth.
Utilities for making it easier to work with OAuth 2.0
credentials.
"""
import os
import threading
from oauth2client import _helpers
from oauth2client import client
class Storage(client.Storage):
"""Store and retrieve a single credential to and from a file."""
def __init__(self, filename):
super(Storage, self).__init__(lock=threading.Lock())
self._filename = filename
def locked_get(self):
"""Retrieve Credential from file.
Returns:
oauth2client.client.Credentials
Raises:
IOError if the file is a symbolic link.
"""
credentials = None
_helpers.validate_file(self._filename)
try:
f = open(self._filename, 'rb')
content = f.read()
f.close()
except IOError:
return credentials
try:
credentials = client.Credentials.new_from_json(content)
credentials.set_store(self)
except ValueError:
pass
return credentials
def _create_file_if_needed(self):
"""Create an empty file if necessary.
This method will not initialize the file. Instead it implements a
simple version of "touch" to ensure the file has been created.
"""
if not os.path.exists(self._filename):
old_umask = os.umask(0o177)
try:
open(self._filename, 'a+b').close()
finally:
os.umask(old_umask)
def locked_put(self, credentials):
"""Write Credentials to file.
Args:
credentials: Credentials, the credentials to store.
Raises:
IOError if the file is a symbolic link.
"""
self._create_file_if_needed()
_helpers.validate_file(self._filename)
f = open(self._filename, 'w')
f.write(credentials.to_json())
f.close()
def locked_delete(self):
"""Delete Credentials file.
Args:
credentials: Credentials, the credentials to store.
"""
os.unlink(self._filename)

View File

@@ -1,685 +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.
"""oauth2client Service account credentials class."""
import base64
import copy
import datetime
import json
import time
import oauth2client
from oauth2client import _helpers
from oauth2client import client
from oauth2client import crypt
from oauth2client import transport
_PASSWORD_DEFAULT = 'notasecret'
_PKCS12_KEY = '_private_key_pkcs12'
_PKCS12_ERROR = r"""
This library only implements PKCS#12 support via the pyOpenSSL library.
Either install pyOpenSSL, or please convert the .p12 file
to .pem format:
$ cat key.p12 | \
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
> openssl rsa > key.pem
"""
class ServiceAccountCredentials(client.AssertionCredentials):
"""Service Account credential for OAuth 2.0 signed JWT grants.
Supports
* JSON keyfile (typically contains a PKCS8 key stored as
PEM text)
* ``.p12`` key (stores PKCS12 key and certificate)
Makes an assertion to server using a signed JWT assertion in exchange
for an access token.
This credential does not require a flow to instantiate because it
represents a two legged flow, and therefore has all of the required
information to generate and refresh its own access tokens.
Args:
service_account_email: string, The email associated with the
service account.
signer: ``crypt.Signer``, A signer which can be used to sign content.
scopes: List or string, (Optional) Scopes to use when acquiring
an access token.
private_key_id: string, (Optional) Private key identifier. Typically
only used with a JSON keyfile. Can be sent in the
header of a JWT token assertion.
client_id: string, (Optional) Client ID for the project that owns the
service account.
user_agent: string, (Optional) User agent to use when sending
request.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
kwargs: dict, Extra key-value pairs (both strings) to send in the
payload body when making an assertion.
"""
MAX_TOKEN_LIFETIME_SECS = 3600
"""Max lifetime of the token (one hour, in seconds)."""
NON_SERIALIZED_MEMBERS = (
frozenset(['_signer']) |
client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
"""Members that aren't serialized when object is converted to JSON."""
# Can be over-ridden by factory constructors. Used for
# serialization/deserialization purposes.
_private_key_pkcs8_pem = None
_private_key_pkcs12 = None
_private_key_password = None
def __init__(self,
service_account_email,
signer,
scopes='',
private_key_id=None,
client_id=None,
user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
**kwargs):
super(ServiceAccountCredentials, self).__init__(
None, user_agent=user_agent, token_uri=token_uri,
revoke_uri=revoke_uri)
self._service_account_email = service_account_email
self._signer = signer
self._scopes = _helpers.scopes_to_string(scopes)
self._private_key_id = private_key_id
self.client_id = client_id
self._user_agent = user_agent
self._kwargs = kwargs
def _to_json(self, strip, to_serialize=None):
"""Utility function that creates JSON repr. of a credentials object.
Over-ride is needed since PKCS#12 keys will not in general be JSON
serializable.
Args:
strip: array, An array of names of members to exclude from the
JSON.
to_serialize: dict, (Optional) The properties for this object
that will be serialized. This allows callers to
modify before serializing.
Returns:
string, a JSON representation of this instance, suitable to pass to
from_json().
"""
if to_serialize is None:
to_serialize = copy.copy(self.__dict__)
pkcs12_val = to_serialize.get(_PKCS12_KEY)
if pkcs12_val is not None:
to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
return super(ServiceAccountCredentials, self)._to_json(
strip, to_serialize=to_serialize)
@classmethod
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
token_uri=None, revoke_uri=None):
"""Helper for factory constructors from JSON keyfile.
Args:
keyfile_dict: dict-like object, The parsed dictionary-like object
containing the contents of the JSON keyfile.
scopes: List or string, Scopes to use when acquiring an
access token.
token_uri: string, URI for OAuth 2.0 provider token endpoint.
If unset and not present in keyfile_dict, defaults
to Google's endpoints.
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
If unset and not present in keyfile_dict, defaults
to Google's endpoints.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile contents.
Raises:
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
KeyError, if one of the expected keys is not present in
the keyfile.
"""
creds_type = keyfile_dict.get('type')
if creds_type != client.SERVICE_ACCOUNT:
raise ValueError('Unexpected credentials type', creds_type,
'Expected', client.SERVICE_ACCOUNT)
service_account_email = keyfile_dict['client_email']
private_key_pkcs8_pem = keyfile_dict['private_key']
private_key_id = keyfile_dict['private_key_id']
client_id = keyfile_dict['client_id']
if not token_uri:
token_uri = keyfile_dict.get('token_uri',
oauth2client.GOOGLE_TOKEN_URI)
if not revoke_uri:
revoke_uri = keyfile_dict.get('revoke_uri',
oauth2client.GOOGLE_REVOKE_URI)
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
credentials = cls(service_account_email, signer, scopes=scopes,
private_key_id=private_key_id,
client_id=client_id, token_uri=token_uri,
revoke_uri=revoke_uri)
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
return credentials
@classmethod
def from_json_keyfile_name(cls, filename, scopes='',
token_uri=None, revoke_uri=None):
"""Factory constructor from JSON keyfile by name.
Args:
filename: string, The location of the keyfile.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for OAuth 2.0 provider token endpoint.
If unset and not present in the key file, defaults
to Google's endpoints.
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
If unset and not present in the key file, defaults
to Google's endpoints.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
KeyError, if one of the expected keys is not present in
the keyfile.
"""
with open(filename, 'r') as file_obj:
client_credentials = json.load(file_obj)
return cls._from_parsed_json_keyfile(client_credentials, scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)
@classmethod
def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
token_uri=None, revoke_uri=None):
"""Factory constructor from parsed JSON keyfile.
Args:
keyfile_dict: dict-like object, The parsed dictionary-like object
containing the contents of the JSON keyfile.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for OAuth 2.0 provider token endpoint.
If unset and not present in keyfile_dict, defaults
to Google's endpoints.
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
If unset and not present in keyfile_dict, defaults
to Google's endpoints.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
KeyError, if one of the expected keys is not present in
the keyfile.
"""
return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)
@classmethod
def _from_p12_keyfile_contents(cls, service_account_email,
private_key_pkcs12,
private_key_password=None, scopes='',
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile.
Args:
service_account_email: string, The email associated with the
service account.
private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
private_key_password: string, (Optional) Password for PKCS#12
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0
provider can be used.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
NotImplementedError if pyOpenSSL is not installed / not the
active crypto library.
"""
if private_key_password is None:
private_key_password = _PASSWORD_DEFAULT
if crypt.Signer is not crypt.OpenSSLSigner:
raise NotImplementedError(_PKCS12_ERROR)
signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password)
credentials = cls(service_account_email, signer, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)
credentials._private_key_pkcs12 = private_key_pkcs12
credentials._private_key_password = private_key_password
return credentials
@classmethod
def from_p12_keyfile(cls, service_account_email, filename,
private_key_password=None, scopes='',
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile.
Args:
service_account_email: string, The email associated with the
service account.
filename: string, The location of the PKCS#12 keyfile.
private_key_password: string, (Optional) Password for PKCS#12
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0
provider can be used.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
NotImplementedError if pyOpenSSL is not installed / not the
active crypto library.
"""
with open(filename, 'rb') as file_obj:
private_key_pkcs12 = file_obj.read()
return cls._from_p12_keyfile_contents(
service_account_email, private_key_pkcs12,
private_key_password=private_key_password, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)
@classmethod
def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
private_key_password=None, scopes='',
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile.
Args:
service_account_email: string, The email associated with the
service account.
file_buffer: stream, A buffer that implements ``read()``
and contains the PKCS#12 key contents.
private_key_password: string, (Optional) Password for PKCS#12
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0
provider can be used.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
NotImplementedError if pyOpenSSL is not installed / not the
active crypto library.
"""
private_key_pkcs12 = file_buffer.read()
return cls._from_p12_keyfile_contents(
service_account_email, private_key_pkcs12,
private_key_password=private_key_password, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)
def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""
now = int(time.time())
payload = {
'aud': self.token_uri,
'scope': self._scopes,
'iat': now,
'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
'iss': self._service_account_email,
}
payload.update(self._kwargs)
return crypt.make_signed_jwt(self._signer, payload,
key_id=self._private_key_id)
def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
Implements abstract method
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
Args:
blob: bytes, Message to be signed.
Returns:
tuple, A pair of the private key ID used to sign the blob and
the signed contents.
"""
return self._private_key_id, self._signer.sign(blob)
@property
def service_account_email(self):
"""Get the email for the current service account.
Returns:
string, The email associated with the service account.
"""
return self._service_account_email
@property
def serialization_data(self):
# NOTE: This is only useful for JSON keyfile.
return {
'type': 'service_account',
'client_email': self._service_account_email,
'private_key_id': self._private_key_id,
'private_key': self._private_key_pkcs8_pem,
'client_id': self.client_id,
}
@classmethod
def from_json(cls, json_data):
"""Deserialize a JSON-serialized instance.
Inverse to :meth:`to_json`.
Args:
json_data: dict or string, Serialized JSON (as a string or an
already parsed dictionary) representing a credential.
Returns:
ServiceAccountCredentials from the serialized data.
"""
if not isinstance(json_data, dict):
json_data = json.loads(_helpers._from_bytes(json_data))
private_key_pkcs8_pem = None
pkcs12_val = json_data.get(_PKCS12_KEY)
password = None
if pkcs12_val is None:
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
else:
# NOTE: This assumes that private_key_pkcs8_pem is not also
# in the serialized data. This would be very incorrect
# state.
pkcs12_val = base64.b64decode(pkcs12_val)
password = json_data['_private_key_password']
signer = crypt.Signer.from_string(pkcs12_val, password)
credentials = cls(
json_data['_service_account_email'],
signer,
scopes=json_data['_scopes'],
private_key_id=json_data['_private_key_id'],
client_id=json_data['client_id'],
user_agent=json_data['_user_agent'],
**json_data['_kwargs']
)
if private_key_pkcs8_pem is not None:
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
if pkcs12_val is not None:
credentials._private_key_pkcs12 = pkcs12_val
if password is not None:
credentials._private_key_password = password
credentials.invalid = json_data['invalid']
credentials.access_token = json_data['access_token']
credentials.token_uri = json_data['token_uri']
credentials.revoke_uri = json_data['revoke_uri']
token_expiry = json_data.get('token_expiry', None)
if token_expiry is not None:
credentials.token_expiry = datetime.datetime.strptime(
token_expiry, client.EXPIRY_FORMAT)
return credentials
def create_scoped_required(self):
return not self._scopes
def create_scoped(self, scopes):
result = self.__class__(self._service_account_email,
self._signer,
scopes=scopes,
private_key_id=self._private_key_id,
client_id=self.client_id,
user_agent=self._user_agent,
**self._kwargs)
result.token_uri = self.token_uri
result.revoke_uri = self.revoke_uri
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
result._private_key_pkcs12 = self._private_key_pkcs12
result._private_key_password = self._private_key_password
return result
def create_with_claims(self, claims):
"""Create credentials that specify additional claims.
Args:
claims: dict, key-value pairs for claims.
Returns:
ServiceAccountCredentials, a copy of the current service account
credentials with updated claims to use when obtaining access
tokens.
"""
new_kwargs = dict(self._kwargs)
new_kwargs.update(claims)
result = self.__class__(self._service_account_email,
self._signer,
scopes=self._scopes,
private_key_id=self._private_key_id,
client_id=self.client_id,
user_agent=self._user_agent,
**new_kwargs)
result.token_uri = self.token_uri
result.revoke_uri = self.revoke_uri
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
result._private_key_pkcs12 = self._private_key_pkcs12
result._private_key_password = self._private_key_password
return result
def create_delegated(self, sub):
"""Create credentials that act as domain-wide delegation of authority.
Use the ``sub`` parameter as the subject to delegate on behalf of
that user.
For example::
>>> account_sub = 'foo@email.com'
>>> delegate_creds = creds.create_delegated(account_sub)
Args:
sub: string, An email address that this service account will
act on behalf of (via domain-wide delegation).
Returns:
ServiceAccountCredentials, a copy of the current service account
updated to act on behalf of ``sub``.
"""
return self.create_with_claims({'sub': sub})
def _datetime_to_secs(utc_time):
# TODO(issue 298): use time_delta.total_seconds()
# time_delta.total_seconds() not supported in Python 2.6
epoch = datetime.datetime(1970, 1, 1)
time_delta = utc_time - epoch
return time_delta.days * 86400 + time_delta.seconds
class _JWTAccessCredentials(ServiceAccountCredentials):
"""Self signed JWT credentials.
Makes an assertion to server using a self signed JWT from service account
credentials. These credentials do NOT use OAuth 2.0 and instead
authenticate directly.
"""
_MAX_TOKEN_LIFETIME_SECS = 3600
"""Max lifetime of the token (one hour, in seconds)."""
def __init__(self,
service_account_email,
signer,
scopes=None,
private_key_id=None,
client_id=None,
user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
additional_claims=None):
if additional_claims is None:
additional_claims = {}
super(_JWTAccessCredentials, self).__init__(
service_account_email,
signer,
private_key_id=private_key_id,
client_id=client_id,
user_agent=user_agent,
token_uri=token_uri,
revoke_uri=revoke_uri,
**additional_claims)
def authorize(self, http):
"""Authorize an httplib2.Http instance with a JWT assertion.
Unless specified, the 'aud' of the assertion will be the base
uri of the request.
Args:
http: An instance of ``httplib2.Http`` or something that acts
like it.
Returns:
A modified instance of http that was passed in.
Example::
h = httplib2.Http()
h = credentials.authorize(h)
"""
transport.wrap_http_for_jwt_access(self, http)
return http
def get_access_token(self, http=None, additional_claims=None):
"""Create a signed jwt.
Args:
http: unused
additional_claims: dict, additional claims to add to
the payload of the JWT.
Returns:
An AccessTokenInfo with the signed jwt
"""
if additional_claims is None:
if self.access_token is None or self.access_token_expired:
self.refresh(None)
return client.AccessTokenInfo(
access_token=self.access_token, expires_in=self._expires_in())
else:
# Create a 1 time token
token, unused_expiry = self._create_token(additional_claims)
return client.AccessTokenInfo(
access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
def revoke(self, http):
"""Cannot revoke JWTAccessCredentials tokens."""
pass
def create_scoped_required(self):
# JWTAccessCredentials are unscoped by definition
return True
def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
# Returns an OAuth2 credentials with the given scope
result = ServiceAccountCredentials(self._service_account_email,
self._signer,
scopes=scopes,
private_key_id=self._private_key_id,
client_id=self.client_id,
user_agent=self._user_agent,
token_uri=token_uri,
revoke_uri=revoke_uri,
**self._kwargs)
if self._private_key_pkcs8_pem is not None:
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
if self._private_key_pkcs12 is not None:
result._private_key_pkcs12 = self._private_key_pkcs12
if self._private_key_password is not None:
result._private_key_password = self._private_key_password
return result
def refresh(self, http):
"""Refreshes the access_token.
The HTTP object is unused since no request needs to be made to
get a new token, it can just be generated locally.
Args:
http: unused HTTP object
"""
self._refresh(None)
def _refresh(self, http):
"""Refreshes the access_token.
Args:
http: unused HTTP object
"""
self.access_token, self.token_expiry = self._create_token()
def _create_token(self, additional_claims=None):
now = client._UTCNOW()
lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS)
expiry = now + lifetime
payload = {
'iat': _datetime_to_secs(now),
'exp': _datetime_to_secs(expiry),
'iss': self._service_account_email,
'sub': self._service_account_email
}
payload.update(self._kwargs)
if additional_claims is not None:
payload.update(additional_claims)
jwt = crypt.make_signed_jwt(self._signer, payload,
key_id=self._private_key_id)
return jwt.decode('ascii'), expiry

View File

@@ -1,256 +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.
"""Command-line tools for authenticating via OAuth 2.0
Do the OAuth 2.0 Web Server dance for a command line application. Stores the
generated credentials in a common file that is used by other example apps in
the same directory.
"""
from __future__ import print_function
import logging
import socket
import sys
from six.moves import BaseHTTPServer
from six.moves import http_client
from six.moves import input
from six.moves import urllib
from oauth2client import _helpers
from oauth2client import client
__all__ = ['argparser', 'run_flow', 'message_if_missing']
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
To make this sample run you will need to populate the client_secrets.json file
found at:
{file_path}
with information from the APIs Console <https://code.google.com/apis/console>.
"""
_FAILED_START_MESSAGE = """
Failed to start a local webserver listening on either port 8080
or port 8090. Please check your firewall settings and locally
running programs that may be blocking or using those ports.
Falling back to --noauth_local_webserver and continuing with
authorization.
"""
_BROWSER_OPENED_MESSAGE = """
Your browser has been opened to visit:
{address}
If your browser is on a different machine then exit and re-run this
application with the command-line parameter
--noauth_local_webserver
"""
_GO_TO_LINK_MESSAGE = """
Go to the following link in your browser:
{address}
"""
def _CreateArgumentParser():
try:
import argparse
except ImportError: # pragma: NO COVER
return None
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--auth_host_name', default='localhost',
help='Hostname when running a local web server.')
parser.add_argument('--noauth_local_webserver', action='store_true',
default=False, help='Do not run a local web server.')
parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
nargs='*', help='Port web server should listen on.')
parser.add_argument(
'--logging_level', default='ERROR',
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
help='Set the logging level of detail.')
return parser
# argparser is an ArgumentParser that contains command-line options expected
# by tools.run(). Pass it in as part of the 'parents' argument to your own
# ArgumentParser.
argparser = _CreateArgumentParser()
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
"""A server to handle OAuth 2.0 redirects back to localhost.
Waits for a single request and parses the query parameters
into query_params and then stops serving.
"""
query_params = {}
class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""A handler for OAuth 2.0 redirects back to localhost.
Waits for a single request and parses the query parameters
into the servers query_params and then stops serving.
"""
def do_GET(self):
"""Handle a GET request.
Parses the query parameters and prints a message
if the flow has completed. Note that we can't detect
if an error occurred.
"""
self.send_response(http_client.OK)
self.send_header('Content-type', 'text/html')
self.end_headers()
parts = urllib.parse.urlparse(self.path)
query = _helpers.parse_unique_urlencoded(parts.query)
self.server.query_params = query
self.wfile.write(
b'<html><head><title>Authentication Status</title></head>')
self.wfile.write(
b'<body><p>The authentication flow has completed.</p>')
self.wfile.write(b'</body></html>')
def log_message(self, format, *args):
"""Do not log messages to stdout while running as cmd. line program."""
@_helpers.positional(3)
def run_flow(flow, storage, flags=None, http=None):
"""Core code for a command-line application.
The ``run()`` function is called from your application and runs
through all the steps to obtain credentials. It takes a ``Flow``
argument and attempts to open an authorization server page in the
user's default web browser. The server asks the user to grant your
application access to the user's data. If the user grants access,
the ``run()`` function returns new credentials. The new credentials
are also stored in the ``storage`` argument, which updates the file
associated with the ``Storage`` object.
It presumes it is run from a command-line application and supports the
following flags:
``--auth_host_name`` (string, default: ``localhost``)
Host name to use when running a local web server to handle
redirects during OAuth authorization.
``--auth_host_port`` (integer, default: ``[8080, 8090]``)
Port to use when running a local web server to handle redirects
during OAuth authorization. Repeat this option to specify a list
of values.
``--[no]auth_local_webserver`` (boolean, default: ``True``)
Run a local web server to handle redirects during OAuth
authorization.
The tools module defines an ``ArgumentParser`` the already contains the
flag definitions that ``run()`` requires. You can pass that
``ArgumentParser`` to your ``ArgumentParser`` constructor::
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
parents=[tools.argparser])
flags = parser.parse_args(argv)
Args:
flow: Flow, an OAuth 2.0 Flow to step through.
storage: Storage, a ``Storage`` to store the credential in.
flags: ``argparse.Namespace``, (Optional) The command-line flags. This
is the object returned from calling ``parse_args()`` on
``argparse.ArgumentParser`` as described above. Defaults
to ``argparser.parse_args()``.
http: An instance of ``httplib2.Http.request`` or something that
acts like it.
Returns:
Credentials, the obtained credential.
"""
if flags is None:
flags = argparser.parse_args()
logging.getLogger().setLevel(getattr(logging, flags.logging_level))
if not flags.noauth_local_webserver:
success = False
port_number = 0
for port in flags.auth_host_port:
port_number = port
try:
httpd = ClientRedirectServer((flags.auth_host_name, port),
ClientRedirectHandler)
except socket.error:
pass
else:
success = True
break
flags.noauth_local_webserver = not success
if not success:
print(_FAILED_START_MESSAGE)
if not flags.noauth_local_webserver:
oauth_callback = 'http://{host}:{port}/'.format(
host=flags.auth_host_name, port=port_number)
else:
oauth_callback = client.OOB_CALLBACK_URN
flow.redirect_uri = oauth_callback
authorize_url = flow.step1_get_authorize_url()
if not flags.noauth_local_webserver:
import webbrowser
webbrowser.open(authorize_url, new=1, autoraise=True)
print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url))
else:
print(_GO_TO_LINK_MESSAGE.format(address=authorize_url))
code = None
if not flags.noauth_local_webserver:
httpd.handle_request()
if 'error' in httpd.query_params:
sys.exit('Authentication request was rejected.')
if 'code' in httpd.query_params:
code = httpd.query_params['code']
else:
print('Failed to find "code" in the query parameters '
'of the redirect.')
sys.exit('Try running with --noauth_local_webserver.')
else:
code = input('Enter verification code: ').strip()
try:
credential = flow.step2_exchange(code, http=http)
except client.FlowExchangeError as e:
sys.exit('Authentication has failed: {0}'.format(e))
storage.put(credential)
credential.set_store(storage)
print('Authentication successful.')
return credential
def message_if_missing(filename):
"""Helpful message to display if the CLIENT_SECRETS file is missing."""
return _CLIENT_SECRETS_MESSAGE.format(file_path=filename)

View File

@@ -1,285 +0,0 @@
# 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 import _helpers
_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(*args, **kwargs):
"""Return a new HTTP object.
Args:
*args: tuple, The positional arguments to be passed when
contructing a new HTTP object.
**kwargs: dict, The keyword arguments to be passed when
contructing a new HTTP object.
Returns:
httplib2.Http, an HTTP object.
"""
return httplib2.Http(*args, **kwargs)
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[_helpers._to_bytes(k)] = _helpers._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 = request(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 = request(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.
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 request(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 request(orig_request_method, uri, method, body,
clean_headers(headers),
redirections, connection_type)
# Replace the request method with our own closure.
http.request = new_request
# Set credentials as a property of the request method.
http.request.credentials = credentials
def request(http, uri, method='GET', body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
connection_type=None):
"""Make an HTTP request with an HTTP object and arguments.
Args:
http: httplib2.Http, an http object to be used to make requests.
uri: string, The URI to be requested.
method: string, The HTTP method to use for the request. Defaults
to 'GET'.
body: string, The payload / body in HTTP request. By default
there is no payload.
headers: dict, Key-value pairs of request headers. By default
there are no headers.
redirections: int, The number of allowed 203 redirects for
the request. Defaults to 5.
connection_type: httplib.HTTPConnection, a subclass to be used for
establishing connection. If not set, the type
will be determined from the ``uri``.
Returns:
tuple, a pair of a httplib2.Response with the status code and other
headers and the bytes of the content returned.
"""
# NOTE: Allowing http or http.request is temporary (See Issue 601).
http_callable = getattr(http, 'request', http)
return http_callable(uri, method=method, body=body, headers=headers,
redirections=redirections,
connection_type=connection_type)
_CACHED_HTTP = httplib2.Http(MemoryCache())