oauth2client 4.0

This commit is contained in:
Jay Lee
2017-01-24 14:00:07 -05:00
parent 6ba0a5d942
commit 4a894958f0
18 changed files with 615 additions and 271 deletions

View File

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

View File

@@ -11,12 +11,248 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Helper functions for commonly used utilities.""" """Helper functions for commonly used utilities."""
import base64 import base64
import functools
import inspect
import json import json
import logging
import os
import warnings
import six 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): def _parse_pem_key(raw_key_input):

65
src/oauth2client/_pkce.py Normal file
View File

@@ -0,0 +1,65 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
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))
# 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.
"""
return base64.urlsafe_b64encode(hashlib.sha256(verifier).digest())

View File

@@ -34,13 +34,11 @@ from six.moves import urllib
import oauth2client import oauth2client
from oauth2client import _helpers from oauth2client import _helpers
from oauth2client import _pkce
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client import transport from oauth2client import transport
from oauth2client import util
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
HAS_OPENSSL = False HAS_OPENSSL = False
HAS_CRYPTO = False HAS_CRYPTO = False
try: try:
@@ -100,20 +98,20 @@ AccessTokenInfo = collections.namedtuple(
DEFAULT_ENV_NAME = 'UNKNOWN' DEFAULT_ENV_NAME = 'UNKNOWN'
# If set to True _get_environment avoid GCE check (_detect_gce_environment) # If set to True _get_environment avoid GCE check (_detect_gce_environment)
NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') NO_GCE_CHECK = os.getenv('NO_GCE_CHECK', 'False')
# Timeout in seconds to wait for the GCE metadata server when detecting the # Timeout in seconds to wait for the GCE metadata server when detecting the
# GCE environment. # GCE environment.
try: try:
GCE_METADATA_TIMEOUT = int( GCE_METADATA_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3))
os.environ.setdefault('GCE_METADATA_TIMEOUT', '3'))
except ValueError: # pragma: NO COVER except ValueError: # pragma: NO COVER
GCE_METADATA_TIMEOUT = 3 GCE_METADATA_TIMEOUT = 3
_SERVER_SOFTWARE = 'SERVER_SOFTWARE' _SERVER_SOFTWARE = 'SERVER_SOFTWARE'
_GCE_METADATA_HOST = '169.254.169.254' _GCE_METADATA_URI = 'http://169.254.169.254'
_METADATA_FLAVOR_HEADER = 'Metadata-Flavor' _METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header
_DESIRED_METADATA_FLAVOR = 'Google' _DESIRED_METADATA_FLAVOR = 'Google'
_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
# Expose utcnow() at module level to allow for # Expose utcnow() at module level to allow for
# easier testing (by replacing with a stub). # easier testing (by replacing with a stub).
@@ -273,7 +271,7 @@ class Credentials(object):
to_serialize[key] = val.decode('utf-8') to_serialize[key] = val.decode('utf-8')
if isinstance(val, set): if isinstance(val, set):
to_serialize[key] = list(val) to_serialize[key] = list(val)
return json.dumps(to_serialize, indent=4, sort_keys=True) return json.dumps(to_serialize)
def to_json(self): def to_json(self):
"""Creating a JSON representation of an instance of Credentials. """Creating a JSON representation of an instance of Credentials.
@@ -440,23 +438,6 @@ class Storage(object):
self.release_lock() self.release_lock()
def _update_query_params(uri, params):
"""Updates a URI with new query parameters.
Args:
uri: string, A valid URI, with potential existing query parameters.
params: dict, A dictionary of query parameters.
Returns:
The same URI but with the new query parameters added.
"""
parts = urllib.parse.urlparse(uri)
query_params = dict(urllib.parse.parse_qsl(parts.query))
query_params.update(params)
new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
return urllib.parse.urlunparse(new_parts)
class OAuth2Credentials(Credentials): class OAuth2Credentials(Credentials):
"""Credentials object for OAuth 2.0. """Credentials object for OAuth 2.0.
@@ -466,7 +447,7 @@ class OAuth2Credentials(Credentials):
OAuth2Credentials objects may be safely pickled and unpickled. OAuth2Credentials objects may be safely pickled and unpickled.
""" """
@util.positional(8) @_helpers.positional(8)
def __init__(self, access_token, client_id, client_secret, refresh_token, def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, revoke_uri=None, token_expiry, token_uri, user_agent, revoke_uri=None,
id_token=None, token_response=None, scopes=None, id_token=None, token_response=None, scopes=None,
@@ -513,7 +494,7 @@ class OAuth2Credentials(Credentials):
self.revoke_uri = revoke_uri self.revoke_uri = revoke_uri
self.id_token = id_token self.id_token = id_token
self.token_response = token_response self.token_response = token_response
self.scopes = set(util.string_to_scopes(scopes or [])) self.scopes = set(_helpers.string_to_scopes(scopes or []))
self.token_info_uri = token_info_uri self.token_info_uri = token_info_uri
# True if the credentials have been revoked or expired and can't be # True if the credentials have been revoked or expired and can't be
@@ -557,7 +538,7 @@ class OAuth2Credentials(Credentials):
http: httplib2.Http, an http object to be used to make the refresh http: httplib2.Http, an http object to be used to make the refresh
request. request.
""" """
self._refresh(http.request) self._refresh(http)
def revoke(self, http): def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void. """Revokes a refresh_token and makes the credentials void.
@@ -566,7 +547,7 @@ class OAuth2Credentials(Credentials):
http: httplib2.Http, an http object to be used to make the revoke http: httplib2.Http, an http object to be used to make the revoke
request. request.
""" """
self._revoke(http.request) self._revoke(http)
def apply(self, headers): def apply(self, headers):
"""Add the authorization to the headers. """Add the authorization to the headers.
@@ -592,7 +573,7 @@ class OAuth2Credentials(Credentials):
not have scopes. In both cases, you can use refresh_scopes() to not have scopes. In both cases, you can use refresh_scopes() to
obtain the canonical set of scopes. obtain the canonical set of scopes.
""" """
scopes = util.string_to_scopes(scopes) scopes = _helpers.string_to_scopes(scopes)
return set(scopes).issubset(self.scopes) return set(scopes).issubset(self.scopes)
def retrieve_scopes(self, http): def retrieve_scopes(self, http):
@@ -607,7 +588,7 @@ class OAuth2Credentials(Credentials):
Returns: Returns:
A set of strings containing the canonical list of scopes. A set of strings containing the canonical list of scopes.
""" """
self._retrieve_scopes(http.request) self._retrieve_scopes(http)
return self.scopes return self.scopes
@classmethod @classmethod
@@ -746,7 +727,7 @@ class OAuth2Credentials(Credentials):
return headers return headers
def _refresh(self, http_request): def _refresh(self, http):
"""Refreshes the access_token. """Refreshes the access_token.
This method first checks by reading the Storage object if available. This method first checks by reading the Storage object if available.
@@ -754,15 +735,13 @@ class OAuth2Credentials(Credentials):
refresh is completed. refresh is completed.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
refresh request.
Raises: Raises:
HttpAccessTokenRefreshError: When the refresh fails. HttpAccessTokenRefreshError: When the refresh fails.
""" """
if not self.store: if not self.store:
self._do_refresh_request(http_request) self._do_refresh_request(http)
else: else:
self.store.acquire_lock() self.store.acquire_lock()
try: try:
@@ -774,17 +753,15 @@ class OAuth2Credentials(Credentials):
logger.info('Updated access_token read from Storage') logger.info('Updated access_token read from Storage')
self._updateFromCredential(new_cred) self._updateFromCredential(new_cred)
else: else:
self._do_refresh_request(http_request) self._do_refresh_request(http)
finally: finally:
self.store.release_lock() self.store.release_lock()
def _do_refresh_request(self, http_request): def _do_refresh_request(self, http):
"""Refresh the access_token using the refresh_token. """Refresh the access_token using the refresh_token.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
refresh request.
Raises: Raises:
HttpAccessTokenRefreshError: When the refresh fails. HttpAccessTokenRefreshError: When the refresh fails.
@@ -793,8 +770,9 @@ class OAuth2Credentials(Credentials):
headers = self._generate_refresh_request_headers() headers = self._generate_refresh_request_headers()
logger.info('Refreshing access_token') logger.info('Refreshing access_token')
resp, content = http_request( resp, content = transport.request(
self.token_uri, method='POST', body=body, headers=headers) http, self.token_uri, method='POST',
body=body, headers=headers)
content = _helpers._from_bytes(content) content = _helpers._from_bytes(content)
if resp.status == http_client.OK: if resp.status == http_client.OK:
d = json.loads(content) d = json.loads(content)
@@ -819,7 +797,7 @@ class OAuth2Credentials(Credentials):
# An {'error':...} response body means the token is expired or # An {'error':...} response body means the token is expired or
# revoked, so we flag the credentials as such. # revoked, so we flag the credentials as such.
logger.info('Failed to retrieve access token: %s', content) logger.info('Failed to retrieve access token: %s', content)
error_msg = 'Invalid response {0}.'.format(resp['status']) error_msg = 'Invalid response {0}.'.format(resp.status)
try: try:
d = json.loads(content) d = json.loads(content)
if 'error' in d: if 'error' in d:
@@ -833,23 +811,19 @@ class OAuth2Credentials(Credentials):
pass pass
raise HttpAccessTokenRefreshError(error_msg, status=resp.status) raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
def _revoke(self, http_request): def _revoke(self, http):
"""Revokes this credential and deletes the stored copy (if it exists). """Revokes this credential and deletes the stored copy (if it exists).
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
revoke request.
""" """
self._do_revoke(http_request, self.refresh_token or self.access_token) self._do_revoke(http, self.refresh_token or self.access_token)
def _do_revoke(self, http_request, token): def _do_revoke(self, http, token):
"""Revokes this credential and deletes the stored copy (if it exists). """Revokes this credential and deletes the stored copy (if it exists).
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
refresh request.
token: A string used as the token to be revoked. Can be either an token: A string used as the token to be revoked. Can be either an
access_token or refresh_token. access_token or refresh_token.
@@ -859,8 +833,13 @@ class OAuth2Credentials(Credentials):
""" """
logger.info('Revoking token') logger.info('Revoking token')
query_params = {'token': token} query_params = {'token': token}
token_revoke_uri = _update_query_params(self.revoke_uri, query_params) token_revoke_uri = _helpers.update_query_params(
resp, content = http_request(token_revoke_uri) self.revoke_uri, query_params)
resp, content = transport.request(http, token_revoke_uri)
if resp.status == http_client.METHOD_NOT_ALLOWED:
body = urllib.parse.urlencode(query_params)
resp, content = transport.request(http, token_revoke_uri,
method='POST', body=body)
if resp.status == http_client.OK: if resp.status == http_client.OK:
self.invalid = True self.invalid = True
else: else:
@@ -876,23 +855,19 @@ class OAuth2Credentials(Credentials):
if self.store: if self.store:
self.store.delete() self.store.delete()
def _retrieve_scopes(self, http_request): def _retrieve_scopes(self, http):
"""Retrieves the list of authorized scopes from the OAuth2 provider. """Retrieves the list of authorized scopes from the OAuth2 provider.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
revoke request.
""" """
self._do_retrieve_scopes(http_request, self.access_token) self._do_retrieve_scopes(http, self.access_token)
def _do_retrieve_scopes(self, http_request, token): def _do_retrieve_scopes(self, http, token):
"""Retrieves the list of authorized scopes from the OAuth2 provider. """Retrieves the list of authorized scopes from the OAuth2 provider.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
refresh request.
token: A string used as the token to identify the credentials to token: A string used as the token to identify the credentials to
the provider. the provider.
@@ -902,13 +877,13 @@ class OAuth2Credentials(Credentials):
""" """
logger.info('Refreshing scopes') logger.info('Refreshing scopes')
query_params = {'access_token': token, 'fields': 'scope'} query_params = {'access_token': token, 'fields': 'scope'}
token_info_uri = _update_query_params(self.token_info_uri, token_info_uri = _helpers.update_query_params(
query_params) self.token_info_uri, query_params)
resp, content = http_request(token_info_uri) resp, content = transport.request(http, token_info_uri)
content = _helpers._from_bytes(content) content = _helpers._from_bytes(content)
if resp.status == http_client.OK: if resp.status == http_client.OK:
d = json.loads(content) d = json.loads(content)
self.scopes = set(util.string_to_scopes(d.get('scope', ''))) self.scopes = set(_helpers.string_to_scopes(d.get('scope', '')))
else: else:
error_msg = 'Invalid response {0}.'.format(resp.status) error_msg = 'Invalid response {0}.'.format(resp.status)
try: try:
@@ -977,19 +952,25 @@ class AccessTokenCredentials(OAuth2Credentials):
data['user_agent']) data['user_agent'])
return retval return retval
def _refresh(self, http_request): def _refresh(self, http):
"""Refreshes the access token.
Args:
http: unused HTTP object.
Raises:
AccessTokenCredentialsError: always
"""
raise AccessTokenCredentialsError( raise AccessTokenCredentialsError(
'The access_token is expired or invalid and can\'t be refreshed.') 'The access_token is expired or invalid and can\'t be refreshed.')
def _revoke(self, http_request): def _revoke(self, http):
"""Revokes the access_token and deletes the store if available. """Revokes the access_token and deletes the store if available.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
revoke request.
""" """
self._do_revoke(http_request, self.access_token) self._do_revoke(http, self.access_token)
def _detect_gce_environment(): def _detect_gce_environment():
@@ -1005,21 +986,16 @@ def _detect_gce_environment():
# could lead to false negatives in the event that we are on GCE, but # could lead to false negatives in the event that we are on GCE, but
# the metadata resolution was particularly slow. The latter case is # the metadata resolution was particularly slow. The latter case is
# "unlikely". # "unlikely".
connection = six.moves.http_client.HTTPConnection( http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT)
_GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT)
try: try:
headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} response, _ = transport.request(
connection.request('GET', '/', headers=headers) http, _GCE_METADATA_URI, headers=_GCE_HEADERS)
response = connection.getresponse() return (
if response.status == http_client.OK: response.status == http_client.OK and
return (response.getheader(_METADATA_FLAVOR_HEADER) == response.get(_METADATA_FLAVOR_HEADER) == _DESIRED_METADATA_FLAVOR)
_DESIRED_METADATA_FLAVOR)
except socket.error: # socket.timeout or socket.error(64, 'Host is down') except socket.error: # socket.timeout or socket.error(64, 'Host is down')
logger.info('Timeout attempting to reach GCE metadata service.') logger.info('Timeout attempting to reach GCE metadata service.')
return False return False
finally:
connection.close()
def _in_gae_environment(): def _in_gae_environment():
@@ -1469,7 +1445,7 @@ class AssertionCredentials(GoogleCredentials):
AssertionCredentials objects may be safely pickled and unpickled. AssertionCredentials objects may be safely pickled and unpickled.
""" """
@util.positional(2) @_helpers.positional(2)
def __init__(self, assertion_type, user_agent=None, def __init__(self, assertion_type, user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
@@ -1511,15 +1487,13 @@ class AssertionCredentials(GoogleCredentials):
"""Generate assertion string to be used in the access token request.""" """Generate assertion string to be used in the access token request."""
raise NotImplementedError raise NotImplementedError
def _revoke(self, http_request): def _revoke(self, http):
"""Revokes the access_token and deletes the store if available. """Revokes the access_token and deletes the store if available.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
revoke request.
""" """
self._do_revoke(http_request, self.access_token) self._do_revoke(http, self.access_token)
def sign_blob(self, blob): def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes). """Cryptographically sign a blob (of bytes).
@@ -1545,7 +1519,7 @@ def _require_crypto_or_die():
raise CryptoUnavailableError('No crypto library available') raise CryptoUnavailableError('No crypto library available')
@util.positional(2) @_helpers.positional(2)
def verify_id_token(id_token, audience, http=None, def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATION_CERTS): cert_uri=ID_TOKEN_VERIFICATION_CERTS):
"""Verifies a signed JWT id_token. """Verifies a signed JWT id_token.
@@ -1572,7 +1546,7 @@ def verify_id_token(id_token, audience, http=None,
if http is None: if http is None:
http = transport.get_cached_http() http = transport.get_cached_http()
resp, content = http.request(cert_uri) resp, content = transport.request(http, cert_uri)
if resp.status == http_client.OK: if resp.status == http_client.OK:
certs = json.loads(_helpers._from_bytes(content)) certs = json.loads(_helpers._from_bytes(content))
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
@@ -1624,7 +1598,7 @@ def _parse_exchange_token_response(content):
except Exception: except Exception:
# different JSON libs raise different exceptions, # different JSON libs raise different exceptions,
# so we just do a catch-all here # so we just do a catch-all here
resp = dict(urllib.parse.parse_qsl(content)) resp = _helpers.parse_unique_urlencoded(content)
# some providers respond with 'expires', others with 'expires_in' # some providers respond with 'expires', others with 'expires_in'
if resp and 'expires' in resp: if resp and 'expires' in resp:
@@ -1633,7 +1607,7 @@ def _parse_exchange_token_response(content):
return resp return resp
@util.positional(4) @_helpers.positional(4)
def credentials_from_code(client_id, client_secret, scope, code, def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None, redirect_uri='postmessage', http=None,
user_agent=None, user_agent=None,
@@ -1641,7 +1615,9 @@ def credentials_from_code(client_id, client_secret, scope, code,
auth_uri=oauth2client.GOOGLE_AUTH_URI, auth_uri=oauth2client.GOOGLE_AUTH_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
device_uri=oauth2client.GOOGLE_DEVICE_URI, device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI): token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
pkce=False,
code_verifier=None):
"""Exchanges an authorization code for an OAuth2Credentials object. """Exchanges an authorization code for an OAuth2Credentials object.
Args: Args:
@@ -1665,6 +1641,15 @@ def credentials_from_code(client_id, client_secret, scope, code,
device_uri: string, URI for device authorization endpoint. For device_uri: string, URI for device authorization endpoint. For
convenience defaults to Google's endpoints but any OAuth convenience defaults to Google's endpoints but any OAuth
2.0 provider can be used. 2.0 provider can be used.
pkce: boolean, default: False, Generate and include a "Proof Key
for Code Exchange" (PKCE) with your authorization and token
requests. This adds security for installed applications that
cannot protect a client_secret. See RFC 7636 for details.
code_verifier: bytestring or None, default: None, parameter passed
as part of the code exchange when pkce=True. If
None, a code_verifier will automatically be
generated as part of step1_get_authorize_url(). See
RFC 7636 for details.
Returns: Returns:
An OAuth2Credentials object. An OAuth2Credentials object.
@@ -1675,16 +1660,20 @@ def credentials_from_code(client_id, client_secret, scope, code,
""" """
flow = OAuth2WebServerFlow(client_id, client_secret, scope, flow = OAuth2WebServerFlow(client_id, client_secret, scope,
redirect_uri=redirect_uri, redirect_uri=redirect_uri,
user_agent=user_agent, auth_uri=auth_uri, user_agent=user_agent,
token_uri=token_uri, revoke_uri=revoke_uri, auth_uri=auth_uri,
token_uri=token_uri,
revoke_uri=revoke_uri,
device_uri=device_uri, device_uri=device_uri,
token_info_uri=token_info_uri) token_info_uri=token_info_uri,
pkce=pkce,
code_verifier=code_verifier)
credentials = flow.step2_exchange(code, http=http) credentials = flow.step2_exchange(code, http=http)
return credentials return credentials
@util.positional(3) @_helpers.positional(3)
def credentials_from_clientsecrets_and_code(filename, scope, code, def credentials_from_clientsecrets_and_code(filename, scope, code,
message=None, message=None,
redirect_uri='postmessage', redirect_uri='postmessage',
@@ -1713,6 +1702,15 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
cache: An optional cache service client that implements get() and set() cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details. methods. See clientsecrets.loadfile() for details.
device_uri: string, OAuth 2.0 device authorization endpoint device_uri: string, OAuth 2.0 device authorization endpoint
pkce: boolean, default: False, Generate and include a "Proof Key
for Code Exchange" (PKCE) with your authorization and token
requests. This adds security for installed applications that
cannot protect a client_secret. See RFC 7636 for details.
code_verifier: bytestring or None, default: None, parameter passed
as part of the code exchange when pkce=True. If
None, a code_verifier will automatically be
generated as part of step1_get_authorize_url(). See
RFC 7636 for details.
Returns: Returns:
An OAuth2Credentials object. An OAuth2Credentials object.
@@ -1803,7 +1801,7 @@ class OAuth2WebServerFlow(Flow):
OAuth2WebServerFlow objects may be safely pickled and unpickled. OAuth2WebServerFlow objects may be safely pickled and unpickled.
""" """
@util.positional(4) @_helpers.positional(4)
def __init__(self, client_id, def __init__(self, client_id,
client_secret=None, client_secret=None,
scope=None, scope=None,
@@ -1816,6 +1814,8 @@ class OAuth2WebServerFlow(Flow):
device_uri=oauth2client.GOOGLE_DEVICE_URI, device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI, token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
authorization_header=None, authorization_header=None,
pkce=False,
code_verifier=None,
**kwargs): **kwargs):
"""Constructor for OAuth2WebServerFlow. """Constructor for OAuth2WebServerFlow.
@@ -1853,6 +1853,15 @@ class OAuth2WebServerFlow(Flow):
require a client to authenticate using a require a client to authenticate using a
header value instead of passing client_secret header value instead of passing client_secret
in the POST body. in the POST body.
pkce: boolean, default: False, Generate and include a "Proof Key
for Code Exchange" (PKCE) with your authorization and token
requests. This adds security for installed applications that
cannot protect a client_secret. See RFC 7636 for details.
code_verifier: bytestring or None, default: None, parameter passed
as part of the code exchange when pkce=True. If
None, a code_verifier will automatically be
generated as part of step1_get_authorize_url(). See
RFC 7636 for details.
**kwargs: dict, The keyword arguments are all optional and required **kwargs: dict, The keyword arguments are all optional and required
parameters for the OAuth calls. parameters for the OAuth calls.
""" """
@@ -1862,7 +1871,7 @@ class OAuth2WebServerFlow(Flow):
raise TypeError("The value of scope must not be None") raise TypeError("The value of scope must not be None")
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
self.scope = util.scopes_to_string(scope) self.scope = _helpers.scopes_to_string(scope)
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.login_hint = login_hint self.login_hint = login_hint
self.user_agent = user_agent self.user_agent = user_agent
@@ -1872,9 +1881,11 @@ class OAuth2WebServerFlow(Flow):
self.device_uri = device_uri self.device_uri = device_uri
self.token_info_uri = token_info_uri self.token_info_uri = token_info_uri
self.authorization_header = authorization_header self.authorization_header = authorization_header
self._pkce = pkce
self.code_verifier = code_verifier
self.params = _oauth2_web_server_flow_params(kwargs) self.params = _oauth2_web_server_flow_params(kwargs)
@util.positional(1) @_helpers.positional(1)
def step1_get_authorize_url(self, redirect_uri=None, state=None): def step1_get_authorize_url(self, redirect_uri=None, state=None):
"""Returns a URI to redirect to the provider. """Returns a URI to redirect to the provider.
@@ -1912,10 +1923,17 @@ class OAuth2WebServerFlow(Flow):
query_params['state'] = state query_params['state'] = state
if self.login_hint is not None: if self.login_hint is not None:
query_params['login_hint'] = self.login_hint query_params['login_hint'] = self.login_hint
query_params.update(self.params) if self._pkce:
return _update_query_params(self.auth_uri, query_params) if not self.code_verifier:
self.code_verifier = _pkce.code_verifier()
challenge = _pkce.code_challenge(self.code_verifier)
query_params['code_challenge'] = challenge
query_params['code_challenge_method'] = 'S256'
@util.positional(1) query_params.update(self.params)
return _helpers.update_query_params(self.auth_uri, query_params)
@_helpers.positional(1)
def step1_get_device_and_user_codes(self, http=None): def step1_get_device_and_user_codes(self, http=None):
"""Returns a user code and the verification URL where to enter it """Returns a user code and the verification URL where to enter it
@@ -1940,8 +1958,8 @@ class OAuth2WebServerFlow(Flow):
if http is None: if http is None:
http = transport.get_http_object() http = transport.get_http_object()
resp, content = http.request(self.device_uri, method='POST', body=body, resp, content = transport.request(
headers=headers) http, self.device_uri, method='POST', body=body, headers=headers)
content = _helpers._from_bytes(content) content = _helpers._from_bytes(content)
if resp.status == http_client.OK: if resp.status == http_client.OK:
try: try:
@@ -1963,7 +1981,7 @@ class OAuth2WebServerFlow(Flow):
pass pass
raise OAuth2DeviceCodeError(error_msg) raise OAuth2DeviceCodeError(error_msg)
@util.positional(2) @_helpers.positional(2)
def step2_exchange(self, code=None, http=None, device_flow_info=None): def step2_exchange(self, code=None, http=None, device_flow_info=None):
"""Exchanges a code for OAuth2Credentials. """Exchanges a code for OAuth2Credentials.
@@ -2006,6 +2024,8 @@ class OAuth2WebServerFlow(Flow):
} }
if self.client_secret is not None: if self.client_secret is not None:
post_data['client_secret'] = self.client_secret post_data['client_secret'] = self.client_secret
if self._pkce:
post_data['code_verifier'] = self.code_verifier
if device_flow_info is not None: if device_flow_info is not None:
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
else: else:
@@ -2023,8 +2043,8 @@ class OAuth2WebServerFlow(Flow):
if http is None: if http is None:
http = transport.get_http_object() http = transport.get_http_object()
resp, content = http.request(self.token_uri, method='POST', body=body, resp, content = transport.request(
headers=headers) http, self.token_uri, method='POST', body=body, headers=headers)
d = _parse_exchange_token_response(content) d = _parse_exchange_token_response(content)
if resp.status == http_client.OK and 'access_token' in d: if resp.status == http_client.OK and 'access_token' in d:
access_token = d['access_token'] access_token = d['access_token']
@@ -2060,10 +2080,10 @@ class OAuth2WebServerFlow(Flow):
raise FlowExchangeError(error_msg) raise FlowExchangeError(error_msg)
@util.positional(2) @_helpers.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None, def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None, login_hint=None, message=None, cache=None, login_hint=None,
device_uri=None): device_uri=None, pkce=None, code_verifier=None):
"""Create a Flow from a clientsecrets file. """Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the Will create the right kind of Flow based on the contents of the
@@ -2112,10 +2132,11 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
'login_hint': login_hint, 'login_hint': login_hint,
} }
revoke_uri = client_info.get('revoke_uri') revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None: optional = ('revoke_uri', 'device_uri', 'pkce', 'code_verifier')
constructor_kwargs['revoke_uri'] = revoke_uri for param in optional:
if device_uri is not None: if locals()[param] is not None:
constructor_kwargs['device_uri'] = device_uri constructor_kwargs[param] = locals()[param]
return OAuth2WebServerFlow( return OAuth2WebServerFlow(
client_info['client_id'], client_info['client_secret'], client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs) scope, **constructor_kwargs)

View File

@@ -22,7 +22,6 @@ import json
import six import six
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
# Properties that make a client_secrets.json file valid. # Properties that make a client_secrets.json file valid.
TYPE_WEB = 'web' TYPE_WEB = 'web'

View File

@@ -20,28 +20,25 @@ See https://cloud.google.com/compute/docs/metadata
import datetime import datetime
import json import json
import httplib2
from six.moves import http_client from six.moves import http_client
from six.moves.urllib import parse as urlparse from six.moves.urllib import parse as urlparse
from oauth2client import _helpers from oauth2client import _helpers
from oauth2client import client from oauth2client import client
from oauth2client import util from oauth2client import transport
METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/' METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
METADATA_HEADERS = {'Metadata-Flavor': 'Google'} METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
def get(http_request, path, root=METADATA_ROOT, recursive=None): def get(http, path, root=METADATA_ROOT, recursive=None):
"""Fetch a resource from the metadata server. """Fetch a resource from the metadata server.
Args: Args:
http: an object to be used to make HTTP requests.
path: A string indicating the resource to retrieve. For example, path: A string indicating the resource to retrieve. For example,
'instance/service-accounts/defualt' 'instance/service-accounts/defualt'
http_request: A callable that matches the method
signature of httplib2.Http.request. Used to make the request to the
metadataserver.
root: A string indicating the full path to the metadata server root. root: A string indicating the full path to the metadata server root.
recursive: A boolean indicating whether to do a recursive query of recursive: A boolean indicating whether to do a recursive query of
metadata. See metadata. See
@@ -51,15 +48,14 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
A dictionary if the metadata server returns JSON, otherwise a string. A dictionary if the metadata server returns JSON, otherwise a string.
Raises: Raises:
httplib2.Httplib2Error if an error corrured while retrieving metadata. http_client.HTTPException if an error corrured while
retrieving metadata.
""" """
url = urlparse.urljoin(root, path) url = urlparse.urljoin(root, path)
url = util._add_query_parameter(url, 'recursive', recursive) url = _helpers._add_query_parameter(url, 'recursive', recursive)
response, content = http_request( response, content = transport.request(
url, http, url, headers=METADATA_HEADERS)
headers=METADATA_HEADERS
)
if response.status == http_client.OK: if response.status == http_client.OK:
decoded = _helpers._from_bytes(content) decoded = _helpers._from_bytes(content)
@@ -68,21 +64,20 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
else: else:
return decoded return decoded
else: else:
raise httplib2.HttpLib2Error( raise http_client.HTTPException(
'Failed to retrieve {0} from the Google Compute Engine' 'Failed to retrieve {0} from the Google Compute Engine'
'metadata service. Response:\n{1}'.format(url, response)) 'metadata service. Response:\n{1}'.format(url, response))
def get_service_account_info(http_request, service_account='default'): def get_service_account_info(http, service_account='default'):
"""Get information about a service account from the metadata server. """Get information about a service account from the metadata server.
Args: Args:
http: an object to be used to make HTTP requests.
service_account: An email specifying the service account for which to service_account: An email specifying the service account for which to
look up information. Default will be information for the "default" look up information. Default will be information for the "default"
service account of the current compute engine instance. service account of the current compute engine instance.
http_request: A callable that matches the method
signature of httplib2.Http.request. Used to make the request to the
metadata server.
Returns: Returns:
A dictionary with information about the specified service account, A dictionary with information about the specified service account,
for example: for example:
@@ -94,21 +89,19 @@ def get_service_account_info(http_request, service_account='default'):
} }
""" """
return get( return get(
http_request, http,
'instance/service-accounts/{0}/'.format(service_account), 'instance/service-accounts/{0}/'.format(service_account),
recursive=True) recursive=True)
def get_token(http_request, service_account='default'): def get_token(http, service_account='default'):
"""Fetch an oauth token for the """Fetch an oauth token for the
Args: Args:
http: an object to be used to make HTTP requests.
service_account: An email specifying the service account this token service_account: An email specifying the service account this token
should represent. Default will be a token for the "default" service should represent. Default will be a token for the "default" service
account of the current compute engine instance. account of the current compute engine instance.
http_request: A callable that matches the method
signature of httplib2.Http.request. Used to make the request to the
metadataserver.
Returns: Returns:
A tuple of (access token, token expiration), where access token is the A tuple of (access token, token expiration), where access token is the
@@ -116,7 +109,7 @@ def get_token(http_request, service_account='default'):
that indicates when the access token will expire. that indicates when the access token will expire.
""" """
token_json = get( token_json = get(
http_request, http,
'instance/service-accounts/{0}/token'.format(service_account)) 'instance/service-accounts/{0}/token'.format(service_account))
token_expiry = client._UTCNOW() + datetime.timedelta( token_expiry = client._UTCNOW() + datetime.timedelta(
seconds=token_json['expires_in']) seconds=token_json['expires_in'])

View File

@@ -29,13 +29,13 @@ from google.appengine.api import memcache
from google.appengine.api import users from google.appengine.api import users
from google.appengine.ext import db from google.appengine.ext import db
from google.appengine.ext.webapp.util import login_required from google.appengine.ext.webapp.util import login_required
import httplib2
import webapp2 as webapp import webapp2 as webapp
import oauth2client import oauth2client
from oauth2client import _helpers
from oauth2client import client from oauth2client import client
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client import util from oauth2client import transport
from oauth2client.contrib import xsrfutil from oauth2client.contrib import xsrfutil
# This is a temporary fix for a Google internal issue. # This is a temporary fix for a Google internal issue.
@@ -45,8 +45,6 @@ except ImportError: # pragma: NO COVER
_appengine_ndb = None _appengine_ndb = None
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
@@ -131,7 +129,7 @@ class AppAssertionCredentials(client.AssertionCredentials):
information to generate and refresh its own access tokens. information to generate and refresh its own access tokens.
""" """
@util.positional(2) @_helpers.positional(2)
def __init__(self, scope, **kwargs): def __init__(self, scope, **kwargs):
"""Constructor for AppAssertionCredentials """Constructor for AppAssertionCredentials
@@ -143,7 +141,7 @@ class AppAssertionCredentials(client.AssertionCredentials):
or unspecified, the default service account for or unspecified, the default service account for
the app is used. the app is used.
""" """
self.scope = util.scopes_to_string(scope) self.scope = _helpers.scopes_to_string(scope)
self._kwargs = kwargs self._kwargs = kwargs
self.service_account_id = kwargs.get('service_account_id', None) self.service_account_id = kwargs.get('service_account_id', None)
self._service_account_email = None self._service_account_email = None
@@ -157,17 +155,15 @@ class AppAssertionCredentials(client.AssertionCredentials):
data = json.loads(json_data) data = json.loads(json_data)
return AppAssertionCredentials(data['scope']) return AppAssertionCredentials(data['scope'])
def _refresh(self, http_request): def _refresh(self, http):
"""Refreshes the access_token. """Refreshes the access token.
Since the underlying App Engine app_identity implementation does its Since the underlying App Engine app_identity implementation does its
own caching we can skip all the storage hoops and just to a refresh own caching we can skip all the storage hoops and just to a refresh
using the API. using the API.
Args: Args:
http_request: callable, a callable that matches the method http: unused HTTP object
signature of httplib2.Http.request, used to make the
refresh request.
Raises: Raises:
AccessTokenRefreshError: When the refresh fails. AccessTokenRefreshError: When the refresh fails.
@@ -305,7 +301,7 @@ class StorageByKeyName(client.Storage):
and that entities are stored by key_name. and that entities are stored by key_name.
""" """
@util.positional(4) @_helpers.positional(4)
def __init__(self, model, key_name, property_name, cache=None, user=None): def __init__(self, model, key_name, property_name, cache=None, user=None):
"""Constructor for Storage. """Constructor for Storage.
@@ -523,7 +519,7 @@ class OAuth2Decorator(object):
flow = property(get_flow, set_flow) flow = property(get_flow, set_flow)
@util.positional(4) @_helpers.positional(4)
def __init__(self, client_id, client_secret, scope, def __init__(self, client_id, client_secret, scope,
auth_uri=oauth2client.GOOGLE_AUTH_URI, auth_uri=oauth2client.GOOGLE_AUTH_URI,
token_uri=oauth2client.GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
@@ -590,7 +586,7 @@ class OAuth2Decorator(object):
self.credentials = None self.credentials = None
self._client_id = client_id self._client_id = client_id
self._client_secret = client_secret self._client_secret = client_secret
self._scope = util.scopes_to_string(scope) self._scope = _helpers.scopes_to_string(scope)
self._auth_uri = auth_uri self._auth_uri = auth_uri
self._token_uri = token_uri self._token_uri = token_uri
self._revoke_uri = revoke_uri self._revoke_uri = revoke_uri
@@ -742,7 +738,8 @@ class OAuth2Decorator(object):
*args: Positional arguments passed to httplib2.Http constructor. *args: Positional arguments passed to httplib2.Http constructor.
**kwargs: Positional arguments passed to httplib2.Http constructor. **kwargs: Positional arguments passed to httplib2.Http constructor.
""" """
return self.credentials.authorize(httplib2.Http(*args, **kwargs)) return self.credentials.authorize(
transport.get_http_object(*args, **kwargs))
@property @property
def callback_path(self): def callback_path(self):
@@ -804,7 +801,7 @@ class OAuth2Decorator(object):
if (decorator._token_response_param and if (decorator._token_response_param and
credentials.token_response): credentials.token_response):
resp_json = json.dumps(credentials.token_response) resp_json = json.dumps(credentials.token_response)
redirect_uri = util._add_query_parameter( redirect_uri = _helpers._add_query_parameter(
redirect_uri, decorator._token_response_param, redirect_uri, decorator._token_response_param,
resp_json) resp_json)
@@ -848,7 +845,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
""" """
@util.positional(3) @_helpers.positional(3)
def __init__(self, filename, scope, message=None, cache=None, **kwargs): def __init__(self, filename, scope, message=None, cache=None, **kwargs):
"""Constructor """Constructor
@@ -891,7 +888,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
self._message = 'Please configure your application for OAuth 2.0.' self._message = 'Please configure your application for OAuth 2.0.'
@util.positional(2) @_helpers.positional(2)
def oauth2decorator_from_clientsecrets(filename, scope, def oauth2decorator_from_clientsecrets(filename, scope,
message=None, cache=None): message=None, cache=None):
"""Creates an OAuth2Decorator populated from a clientsecrets file. """Creates an OAuth2Decorator populated from a clientsecrets file.

View File

@@ -117,7 +117,12 @@ class DevshellCredentials(client.GoogleCredentials):
user_agent) user_agent)
self._refresh(None) self._refresh(None)
def _refresh(self, http_request): def _refresh(self, http):
"""Refreshes the access token.
Args:
http: unused HTTP object
"""
self.devshell_response = _SendRecv() self.devshell_response = _SendRecv()
self.access_token = self.devshell_response.access_token self.access_token = self.devshell_response.access_token
expires_in = self.devshell_response.expires_in expires_in = self.devshell_response.expires_in

View File

@@ -52,6 +52,9 @@ Add the helper to your INSTALLED_APPS:
This helper also requires the Django Session Middleware, so This helper also requires the Django Session Middleware, so
``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well. ``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 Add the client secrets created earlier to the settings. You can either
specify the path to the credentials file in JSON format specify the path to the credentials file in JSON format
@@ -228,10 +231,10 @@ import importlib
import django.conf import django.conf
from django.core import exceptions from django.core import exceptions
from django.core import urlresolvers from django.core import urlresolvers
import httplib2
from six.moves.urllib import parse from six.moves.urllib import parse
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client.contrib import dictionary_storage from oauth2client.contrib import dictionary_storage
from oauth2client.contrib.django_util import storage from oauth2client.contrib.django_util import storage
@@ -335,15 +338,25 @@ class OAuth2Settings(object):
self.request_prefix = getattr(settings_instance, self.request_prefix = getattr(settings_instance,
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE', 'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE) GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
self.client_id, self.client_secret = \ info = _get_oauth2_client_id_and_secret(settings_instance)
_get_oauth2_client_id_and_secret(settings_instance) self.client_id, self.client_secret = info
if ('django.contrib.sessions.middleware.SessionMiddleware' # Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE
not in settings_instance.MIDDLEWARE_CLASSES): 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( raise exceptions.ImproperlyConfigured(
'The Google OAuth2 Helper requires session middleware to ' 'The Google OAuth2 Helper requires session middleware to '
'be installed. Edit your MIDDLEWARE_CLASSES setting' 'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE '
' to include \'django.contrib.sessions.middleware.' 'setting to include \'django.contrib.sessions.middleware.'
'SessionMiddleware\'.') 'SessionMiddleware\'.')
(self.storage_model, self.storage_model_user_property, (self.storage_model, self.storage_model_user_property,
self.storage_model_credentials_property) = _get_storage_model() self.storage_model_credentials_property) = _get_storage_model()
@@ -470,8 +483,7 @@ class UserOAuth2(object):
@property @property
def http(self): def http(self):
"""Helper method to create an HTTP client authorized with OAuth2 """Helper: create HTTP client authorized with OAuth2 credentials."""
credentials."""
if self.has_credentials(): if self.has_credentials():
return self.credentials.authorize(httplib2.Http()) return self.credentials.authorize(transport.get_http_object())
return None return None

View File

@@ -22,13 +22,13 @@ in the configured storage."""
import hashlib import hashlib
import json import json
import os import os
import pickle
from django import http from django import http
from django import shortcuts from django import shortcuts
from django.conf import settings from django.conf import settings
from django.core import urlresolvers from django.core import urlresolvers
from django.shortcuts import redirect from django.shortcuts import redirect
import jsonpickle
from six.moves.urllib import parse from six.moves.urllib import parse
from oauth2client import client from oauth2client import client
@@ -71,7 +71,7 @@ def _make_flow(request, scopes, return_url=None):
urlresolvers.reverse("google_oauth:callback"))) urlresolvers.reverse("google_oauth:callback")))
flow_key = _FLOW_KEY.format(csrf_token) flow_key = _FLOW_KEY.format(csrf_token)
request.session[flow_key] = pickle.dumps(flow) request.session[flow_key] = jsonpickle.encode(flow)
return flow return flow
@@ -89,7 +89,7 @@ def _get_flow_for_token(csrf_token, request):
CSRF token. CSRF token.
""" """
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None) flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
return None if flow_pickle is None else pickle.loads(flow_pickle) return None if flow_pickle is None else jsonpickle.decode(flow_pickle)
def oauth2_callback(request): def oauth2_callback(request):
@@ -170,7 +170,10 @@ def oauth2_authorize(request):
A redirect to Google OAuth2 Authorization. A redirect to Google OAuth2 Authorization.
""" """
return_url = request.GET.get('return_url', None) 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 # Model storage (but not session storage) requires a logged in user
if django_util.oauth2_settings.storage_model: if django_util.oauth2_settings.storage_model:
if not request.user.is_authenticated(): if not request.user.is_authenticated():
@@ -178,13 +181,11 @@ def oauth2_authorize(request):
settings.LOGIN_URL, parse.quote(request.get_full_path()))) settings.LOGIN_URL, parse.quote(request.get_full_path())))
# This checks for the case where we ended up here because of a logged # 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 # out user but we had credentials for it in the first place
elif get_storage(request).get() is not None: else:
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
if user_oauth.has_credentials():
return redirect(return_url) return redirect(return_url)
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
if not return_url:
return_url = request.META.get('HTTP_REFERER', '/')
flow = _make_flow(request=request, scopes=scopes, return_url=return_url) flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
auth_url = flow.step1_get_authorize_url() auth_url = flow.step1_get_authorize_url()
return shortcuts.redirect(auth_url) return shortcuts.redirect(auth_url)

View File

@@ -179,16 +179,14 @@ try:
except ImportError: # pragma: NO COVER except ImportError: # pragma: NO COVER
raise ImportError('The flask utilities require flask 0.9 or newer.') raise ImportError('The flask utilities require flask 0.9 or newer.')
import httplib2
import six.moves.http_client as httplib import six.moves.http_client as httplib
from oauth2client import client from oauth2client import client
from oauth2client import clientsecrets from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client.contrib import dictionary_storage from oauth2client.contrib import dictionary_storage
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
_DEFAULT_SCOPES = ('email',) _DEFAULT_SCOPES = ('email',)
_CREDENTIALS_KEY = 'google_oauth2_credentials' _CREDENTIALS_KEY = 'google_oauth2_credentials'
_FLOW_KEY = 'google_oauth2_flow_{0}' _FLOW_KEY = 'google_oauth2_flow_{0}'
@@ -553,4 +551,5 @@ class UserOAuth2(object):
""" """
if not self.credentials: if not self.credentials:
raise ValueError('No credentials available.') raise ValueError('No credentials available.')
return self.credentials.authorize(httplib2.Http(*args, **kwargs)) return self.credentials.authorize(
transport.get_http_object(*args, **kwargs))

View File

@@ -20,14 +20,12 @@ Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
import logging import logging
import warnings import warnings
import httplib2 from six.moves import http_client
from oauth2client import client from oauth2client import client
from oauth2client.contrib import _metadata from oauth2client.contrib import _metadata
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SCOPES_WARNING = """\ _SCOPES_WARNING = """\
@@ -98,44 +96,40 @@ class AppAssertionCredentials(client.AssertionCredentials):
Returns: Returns:
A set of strings containing the canonical list of scopes. A set of strings containing the canonical list of scopes.
""" """
self._retrieve_info(http.request) self._retrieve_info(http)
return self.scopes return self.scopes
def _retrieve_info(self, http_request): def _retrieve_info(self, http):
"""Validates invalid service accounts by retrieving service account info. """Retrieves service account info for invalid credentials.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make the
request to the metadata server
""" """
if self.invalid: if self.invalid:
info = _metadata.get_service_account_info( info = _metadata.get_service_account_info(
http_request, http,
service_account=self.service_account_email or 'default') service_account=self.service_account_email or 'default')
self.invalid = False self.invalid = False
self.service_account_email = info['email'] self.service_account_email = info['email']
self.scopes = info['scopes'] self.scopes = info['scopes']
def _refresh(self, http_request): def _refresh(self, http):
"""Refreshes the access_token. """Refreshes the access token.
Skip all the storage hoops and just refresh using the API. Skip all the storage hoops and just refresh using the API.
Args: Args:
http_request: callable, a callable that matches the method http: an object to be used to make HTTP requests.
signature of httplib2.Http.request, used to make
the refresh request.
Raises: Raises:
HttpAccessTokenRefreshError: When the refresh fails. HttpAccessTokenRefreshError: When the refresh fails.
""" """
try: try:
self._retrieve_info(http_request) self._retrieve_info(http)
self.access_token, self.token_expiry = _metadata.get_token( self.access_token, self.token_expiry = _metadata.get_token(
http_request, service_account=self.service_account_email) http, service_account=self.service_account_email)
except httplib2.HttpLib2Error as e: except http_client.HTTPException as err:
raise client.HttpAccessTokenRefreshError(str(e)) raise client.HttpAccessTokenRefreshError(str(err))
@property @property
def serialization_data(self): def serialization_data(self):

View File

@@ -24,9 +24,6 @@ import keyring
from oauth2client import client from oauth2client import client
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
class Storage(client.Storage): class Storage(client.Storage):
"""Store and retrieve a single credential to and from the keyring. """Store and retrieve a single credential to and from the keyring.

View File

@@ -20,12 +20,7 @@ import hmac
import time import time
from oauth2client import _helpers from oauth2client import _helpers
from oauth2client import util
__authors__ = [
'"Doug Coker" <dcoker@google.com>',
'"Joe Gregorio" <jcgregorio@google.com>',
]
# Delimiter character # Delimiter character
DELIMITER = b':' DELIMITER = b':'
@@ -34,7 +29,7 @@ DELIMITER = b':'
DEFAULT_TIMEOUT_SECS = 60 * 60 DEFAULT_TIMEOUT_SECS = 60 * 60
@util.positional(2) @_helpers.positional(2)
def generate_token(key, user_id, action_id='', when=None): def generate_token(key, user_id, action_id='', when=None):
"""Generates a URL-safe token for the given user, action, time tuple. """Generates a URL-safe token for the given user, action, time tuple.
@@ -62,7 +57,7 @@ def generate_token(key, user_id, action_id='', when=None):
return token return token
@util.positional(3) @_helpers.positional(3)
def validate_token(key, token, user_id, action_id="", current_time=None): def validate_token(key, token, user_id, action_id="", current_time=None):
"""Validates that the given token authorizes the user for the action. """Validates that the given token authorizes the user for the action.

View File

@@ -21,16 +21,10 @@ credentials.
import os import os
import threading import threading
from oauth2client import _helpers
from oauth2client import client from oauth2client import client
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
class CredentialsFileSymbolicLinkError(Exception):
"""Credentials files must not be symbolic links."""
class Storage(client.Storage): class Storage(client.Storage):
"""Store and retrieve a single credential to and from a file.""" """Store and retrieve a single credential to and from a file."""
@@ -38,11 +32,6 @@ class Storage(client.Storage):
super(Storage, self).__init__(lock=threading.Lock()) super(Storage, self).__init__(lock=threading.Lock())
self._filename = filename self._filename = filename
def _validate_file(self):
if os.path.islink(self._filename):
raise CredentialsFileSymbolicLinkError(
'File: {0} is a symbolic link.'.format(self._filename))
def locked_get(self): def locked_get(self):
"""Retrieve Credential from file. """Retrieve Credential from file.
@@ -50,10 +39,10 @@ class Storage(client.Storage):
oauth2client.client.Credentials oauth2client.client.Credentials
Raises: Raises:
CredentialsFileSymbolicLinkError if the file is a symbolic link. IOError if the file is a symbolic link.
""" """
credentials = None credentials = None
self._validate_file() _helpers.validate_file(self._filename)
try: try:
f = open(self._filename, 'rb') f = open(self._filename, 'rb')
content = f.read() content = f.read()
@@ -89,10 +78,10 @@ class Storage(client.Storage):
credentials: Credentials, the credentials to store. credentials: Credentials, the credentials to store.
Raises: Raises:
CredentialsFileSymbolicLinkError if the file is a symbolic link. IOError if the file is a symbolic link.
""" """
self._create_file_if_needed() self._create_file_if_needed()
self._validate_file() _helpers.validate_file(self._filename)
f = open(self._filename, 'w') f = open(self._filename, 'w')
f.write(credentials.to_json()) f.write(credentials.to_json())
f.close() f.close()

View File

@@ -25,7 +25,6 @@ from oauth2client import _helpers
from oauth2client import client from oauth2client import client
from oauth2client import crypt from oauth2client import crypt
from oauth2client import transport from oauth2client import transport
from oauth2client import util
_PASSWORD_DEFAULT = 'notasecret' _PASSWORD_DEFAULT = 'notasecret'
@@ -110,7 +109,7 @@ class ServiceAccountCredentials(client.AssertionCredentials):
self._service_account_email = service_account_email self._service_account_email = service_account_email
self._signer = signer self._signer = signer
self._scopes = util.scopes_to_string(scopes) self._scopes = _helpers.scopes_to_string(scopes)
self._private_key_id = private_key_id self._private_key_id = private_key_id
self.client_id = client_id self.client_id = client_id
self._user_agent = user_agent self._user_agent = user_agent
@@ -650,9 +649,22 @@ class _JWTAccessCredentials(ServiceAccountCredentials):
return result return result
def refresh(self, http): 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) self._refresh(None)
def _refresh(self, http_request): def _refresh(self, http):
"""Refreshes the access_token.
Args:
http: unused HTTP object
"""
self.access_token, self.token_expiry = self._create_token() self.access_token, self.token_expiry = self._create_token()
def _create_token(self, additional_claims=None): def _create_token(self, additional_claims=None):

View File

@@ -30,11 +30,10 @@ from six.moves import http_client
from six.moves import input from six.moves import input
from six.moves import urllib from six.moves import urllib
from oauth2client import _helpers
from oauth2client import client from oauth2client import client
from oauth2client import util
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
__all__ = ['argparser', 'run_flow', 'message_if_missing'] __all__ = ['argparser', 'run_flow', 'message_if_missing']
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
@@ -123,22 +122,22 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if an error occurred. if an error occurred.
""" """
self.send_response(http_client.OK) self.send_response(http_client.OK)
self.send_header("Content-type", "text/html") self.send_header('Content-type', 'text/html')
self.end_headers() self.end_headers()
query = self.path.split('?', 1)[-1] parts = urllib.parse.urlparse(self.path)
query = dict(urllib.parse.parse_qsl(query)) query = _helpers.parse_unique_urlencoded(parts.query)
self.server.query_params = query self.server.query_params = query
self.wfile.write( self.wfile.write(
b"<html><head><title>Authentication Status</title></head>") b'<html><head><title>Authentication Status</title></head>')
self.wfile.write( self.wfile.write(
b"<body><p>The authentication flow has completed.</p>") b'<body><p>The authentication flow has completed.</p>')
self.wfile.write(b"</body></html>") self.wfile.write(b'</body></html>')
def log_message(self, format, *args): def log_message(self, format, *args):
"""Do not log messages to stdout while running as cmd. line program.""" """Do not log messages to stdout while running as cmd. line program."""
@util.positional(3) @_helpers.positional(3)
def run_flow(flow, storage, flags=None, http=None): def run_flow(flow, storage, flags=None, http=None):
"""Core code for a command-line application. """Core code for a command-line application.
@@ -218,16 +217,6 @@ def run_flow(flow, storage, flags=None, http=None):
flow.redirect_uri = oauth_callback flow.redirect_uri = oauth_callback
authorize_url = flow.step1_get_authorize_url() authorize_url = flow.step1_get_authorize_url()
if flags.short_url:
try:
from googleapiclient.discovery import build
service = build('urlshortener', 'v1', http=http)
url_result = service.url().insert(body={'longUrl': authorize_url},
key=u'AIzaSyBlmgbii8QfJSYmC9VTMOfqrAt5Vj5wtzE').execute()
authorize_url = url_result['id']
except:
pass
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
import webbrowser import webbrowser
webbrowser.open(authorize_url, new=1, autoraise=True) webbrowser.open(authorize_url, new=1, autoraise=True)

View File

@@ -18,7 +18,7 @@ import httplib2
import six import six
from six.moves import http_client from six.moves import http_client
from oauth2client._helpers import _to_bytes from oauth2client import _helpers
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -58,13 +58,19 @@ def get_cached_http():
return _CACHED_HTTP return _CACHED_HTTP
def get_http_object(): def get_http_object(*args, **kwargs):
"""Return a new HTTP object. """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: Returns:
httplib2.Http, an HTTP object. httplib2.Http, an HTTP object.
""" """
return httplib2.Http() return httplib2.Http(*args, **kwargs)
def _initialize_headers(headers): def _initialize_headers(headers):
@@ -121,7 +127,7 @@ def clean_headers(headers):
k = str(k) k = str(k)
if not isinstance(v, six.binary_type): if not isinstance(v, six.binary_type):
v = str(v) v = str(v)
clean[_to_bytes(k)] = _to_bytes(v) clean[_helpers._to_bytes(k)] = _helpers._to_bytes(v)
except UnicodeEncodeError: except UnicodeEncodeError:
from oauth2client.client import NonAsciiHeaderError from oauth2client.client import NonAsciiHeaderError
raise NonAsciiHeaderError(k, ': ', v) raise NonAsciiHeaderError(k, ': ', v)
@@ -164,7 +170,7 @@ def wrap_http_for_auth(credentials, http):
_STREAM_PROPERTIES): _STREAM_PROPERTIES):
body_stream_position = body.tell() body_stream_position = body.tell()
resp, content = orig_request_method(uri, method, body, resp, content = request(orig_request_method, uri, method, body,
clean_headers(headers), clean_headers(headers),
redirections, connection_type) redirections, connection_type)
@@ -182,7 +188,7 @@ def wrap_http_for_auth(credentials, http):
if body_stream_position is not None: if body_stream_position is not None:
body.seek(body_stream_position) body.seek(body_stream_position)
resp, content = orig_request_method(uri, method, body, resp, content = request(orig_request_method, uri, method, body,
clean_headers(headers), clean_headers(headers),
redirections, connection_type) redirections, connection_type)
@@ -192,7 +198,7 @@ def wrap_http_for_auth(credentials, http):
http.request = new_request http.request = new_request
# Set credentials as a property of the request method. # Set credentials as a property of the request method.
setattr(http.request, 'credentials', credentials) http.request.credentials = credentials
def wrap_http_for_jwt_access(credentials, http): def wrap_http_for_jwt_access(credentials, http):
@@ -222,8 +228,8 @@ def wrap_http_for_jwt_access(credentials, http):
if (credentials.access_token is None or if (credentials.access_token is None or
credentials.access_token_expired): credentials.access_token_expired):
credentials.refresh(None) credentials.refresh(None)
return authenticated_request_method(uri, method, body, return request(authenticated_request_method, uri,
headers, redirections, method, body, headers, redirections,
connection_type) connection_type)
else: else:
# If we don't have an 'aud' (audience) claim, # If we don't have an 'aud' (audience) claim,
@@ -234,12 +240,46 @@ def wrap_http_for_jwt_access(credentials, http):
token, unused_expiry = credentials._create_token({'aud': uri_root}) token, unused_expiry = credentials._create_token({'aud': uri_root})
headers['Authorization'] = 'Bearer ' + token headers['Authorization'] = 'Bearer ' + token
return orig_request_method(uri, method, body, return request(orig_request_method, uri, method, body,
clean_headers(headers), clean_headers(headers),
redirections, connection_type) redirections, connection_type)
# Replace the request method with our own closure. # Replace the request method with our own closure.
http.request = new_request 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()) _CACHED_HTTP = httplib2.Http(MemoryCache())