mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-04 12:51:36 +00:00
Delete duplicated oauth2client library (#852)
This commit is contained in:
@@ -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'
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
@@ -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())
|
|
||||||
@@ -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'=')
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
@@ -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))
|
|
||||||
@@ -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.
|
|
||||||
"""
|
|
||||||
@@ -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'
|
|
||||||
@@ -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
|
|
||||||
@@ -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("'", ''')
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -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.')
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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"])
|
|
||||||
@@ -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")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
@@ -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))
|
|
||||||
@@ -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')
|
|
||||||
@@ -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, '')
|
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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())
|
|
||||||
Reference in New Issue
Block a user