Commit 7e3b6fdd authored by sheyang@google.com's avatar sheyang@google.com

Upgrade 3rd packages

BUG=461614
R=nodir@chromium.org

Review URL: https://codereview.chromium.org/1085893002

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294835 0039d316-1c4b-4281-b951-d872f2087c98
parent ed15219e
diff --git a/third_party/google_api_python_client/apiclient/__init__.py b/third_party/google_api_python_client/apiclient/__init__.py
index 5efb142..acb1a23 100644
--- a/third_party/google_api_python_client/apiclient/__init__.py
+++ b/third_party/google_api_python_client/apiclient/__init__.py
@@ -3,7 +3,7 @@
import googleapiclient
try:
- import oauth2client
+ from third_party import oauth2client
except ImportError:
raise RuntimeError(
'Previous version of google-api-python-client detected; due to a '
diff --git a/third_party/google_api_python_client/googleapiclient/channel.py b/third_party/google_api_python_client/googleapiclient/channel.py
index 68a3b89..4626094 100644
--- a/third_party/google_api_python_client/googleapiclient/channel.py
+++ b/third_party/google_api_python_client/googleapiclient/channel.py
@@ -60,7 +60,7 @@ import datetime
import uuid
from googleapiclient import errors
-from ...oauth2client import util
+from third_party.oauth2client import util
# The unix time epoch starts at midnight 1970.
diff --git a/third_party/google_api_python_client/googleapiclient/discovery.py b/third_party/google_api_python_client/googleapiclient/discovery.py
index 3ddac57..0e9e5cf 100644
--- a/third_party/google_api_python_client/googleapiclient/discovery.py
+++ b/third_party/google_api_python_client/googleapiclient/discovery.py
@@ -47,9 +47,9 @@ except ImportError:
from cgi import parse_qsl
# Third-party imports
-from ... import httplib2
+from third_party import httplib2
+from third_party.uritemplate import uritemplate
import mimeparse
-from ... import uritemplate
# Local imports
from googleapiclient.errors import HttpError
@@ -65,9 +65,9 @@ from googleapiclient.model import JsonModel
from googleapiclient.model import MediaModel
from googleapiclient.model import RawModel
from googleapiclient.schema import Schemas
-from oauth2client.client import GoogleCredentials
-from oauth2client.util import _add_query_parameter
-from oauth2client.util import positional
+from third_party.oauth2client.client import GoogleCredentials
+from third_party.oauth2client.util import _add_query_parameter
+from third_party.oauth2client.util import positional
# The client library requires a version of httplib2 that supports RETRIES.
diff --git a/third_party/google_api_python_client/googleapiclient/errors.py b/third_party/google_api_python_client/googleapiclient/errors.py
index a1999fd..18c52e6 100644
--- a/third_party/google_api_python_client/googleapiclient/errors.py
+++ b/third_party/google_api_python_client/googleapiclient/errors.py
@@ -24,7 +24,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import json
-from ...oauth2client import util
+from third_party.oauth2client import util
class Error(Exception):
diff --git a/third_party/google_api_python_client/googleapiclient/http.py b/third_party/google_api_python_client/googleapiclient/http.py
index 8638279..d2ce70c 100644
--- a/third_party/google_api_python_client/googleapiclient/http.py
+++ b/third_party/google_api_python_client/googleapiclient/http.py
@@ -25,7 +25,6 @@ import StringIO
import base64
import copy
import gzip
-import httplib2
import json
import logging
import mimeparse
@@ -49,7 +48,8 @@ from errors import ResumableUploadError
from errors import UnexpectedBodyError
from errors import UnexpectedMethodError
from model import JsonModel
-from ...oauth2client import util
+from third_party import httplib2
+from third_party.oauth2client import util
DEFAULT_CHUNK_SIZE = 512*1024
diff --git a/third_party/google_api_python_client/googleapiclient/sample_tools.py b/third_party/google_api_python_client/googleapiclient/sample_tools.py
index cbd6d6f..cc0790b 100644
--- a/third_party/google_api_python_client/googleapiclient/sample_tools.py
+++ b/third_party/google_api_python_client/googleapiclient/sample_tools.py
@@ -22,13 +22,13 @@ __all__ = ['init']
import argparse
-import httplib2
import os
from googleapiclient import discovery
-from ...oauth2client import client
-from ...oauth2client import file
-from ...oauth2client import tools
+from third_party import httplib2
+from third_party.oauth2client import client
+from third_party.oauth2client import file
+from third_party.oauth2client import tools
def init(argv, name, version, doc, filename, scope=None, parents=[], discovery_filename=None):
diff --git a/third_party/google_api_python_client/googleapiclient/schema.py b/third_party/google_api_python_client/googleapiclient/schema.py
index af41317..92543ec 100644
--- a/third_party/google_api_python_client/googleapiclient/schema.py
+++ b/third_party/google_api_python_client/googleapiclient/schema.py
@@ -63,7 +63,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import copy
-from oauth2client import util
+from third_party.oauth2client import util
class Schemas(object):
......@@ -3,4 +3,8 @@ Version: v1.3.1
Revision: 49d45a6c3318b75e551c3022020f46c78655f365
License: Apache License, Version 2.0 (the "License")
No local changes
Local modifications:
See also MODIFICATIONS.diff
Notes:
Requires the httplib2 and oauth2client library.
......@@ -3,7 +3,7 @@
import googleapiclient
try:
import oauth2client
from third_party import oauth2client
except ImportError:
raise RuntimeError(
'Previous version of google-api-python-client detected; due to a '
......
......@@ -60,7 +60,7 @@ import datetime
import uuid
from googleapiclient import errors
from ...oauth2client import util
from third_party.oauth2client import util
# The unix time epoch starts at midnight 1970.
......
......@@ -47,9 +47,9 @@ except ImportError:
from cgi import parse_qsl
# Third-party imports
from ... import httplib2
from third_party import httplib2
from third_party.uritemplate import uritemplate
import mimeparse
from ... import uritemplate
# Local imports
from googleapiclient.errors import HttpError
......@@ -65,9 +65,9 @@ from googleapiclient.model import JsonModel
from googleapiclient.model import MediaModel
from googleapiclient.model import RawModel
from googleapiclient.schema import Schemas
from oauth2client.client import GoogleCredentials
from oauth2client.util import _add_query_parameter
from oauth2client.util import positional
from third_party.oauth2client.client import GoogleCredentials
from third_party.oauth2client.util import _add_query_parameter
from third_party.oauth2client.util import positional
# The client library requires a version of httplib2 that supports RETRIES.
......
......@@ -24,7 +24,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import json
from ...oauth2client import util
from third_party.oauth2client import util
class Error(Exception):
......
......@@ -25,7 +25,6 @@ import StringIO
import base64
import copy
import gzip
import httplib2
import json
import logging
import mimeparse
......@@ -49,7 +48,8 @@ from errors import ResumableUploadError
from errors import UnexpectedBodyError
from errors import UnexpectedMethodError
from model import JsonModel
from ...oauth2client import util
from third_party import httplib2
from third_party.oauth2client import util
DEFAULT_CHUNK_SIZE = 512*1024
......
......@@ -22,13 +22,13 @@ __all__ = ['init']
import argparse
import httplib2
import os
from googleapiclient import discovery
from ...oauth2client import client
from ...oauth2client import file
from ...oauth2client import tools
from third_party import httplib2
from third_party.oauth2client import client
from third_party.oauth2client import file
from third_party.oauth2client import tools
def init(argv, name, version, doc, filename, scope=None, parents=[], discovery_filename=None):
......
......@@ -63,7 +63,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import copy
from oauth2client import util
from third_party.oauth2client import util
class Schemas(object):
......
Name: oauth2client
Short Name: oauth2client
URL: https://pypi.python.org/packages/source/o/oauth2client/oauth2client-1.2.tar.gz
Version: 1.2
URL: https://pypi.python.org/packages/source/o/oauth2client/oauth2client-1.4.7.tar.gz
Version: 1.4.7
License: Apache License 2.0
Description:
......
__version__ = "1.2"
"""Client library for using OAuth2, especially with Google APIs."""
__version__ = '1.4.7'
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth'
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'
\ No newline at end of file
# Copyright (C) 2010 Google Inc.
#
# 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 module to import a JSON module
Hides all the messy details of exactly where
we get a simplejson module from.
"""
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
try: # pragma: no cover
# Should work for Python2.6 and higher.
import json as simplejson
except ImportError: # pragma: no cover
try:
import simplejson
except ImportError:
# Try to import from django, should work on App Engine
from django.utils import simplejson
# Copyright (C) 2010 Google Inc.
# 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.
......@@ -19,14 +19,14 @@ Utilities for making it easier to use OAuth 2.0 on Google App Engine.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import base64
import cgi
import httplib2
import json
import logging
import os
import pickle
import threading
import time
import httplib2
from google.appengine.api import app_identity
from google.appengine.api import memcache
......@@ -41,7 +41,6 @@ from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import clientsecrets
from oauth2client import util
from oauth2client import xsrfutil
from oauth2client.anyjson import simplejson
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import AssertionCredentials
from oauth2client.client import Credentials
......@@ -159,15 +158,20 @@ class AppAssertionCredentials(AssertionCredentials):
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 = util.scopes_to_string(scope)
self._kwargs = kwargs
self.service_account_id = kwargs.get('service_account_id', 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 = simplejson.loads(json)
def from_json(cls, json_data):
data = json.loads(json_data)
return AppAssertionCredentials(data['scope'])
def _refresh(self, http_request):
......@@ -186,11 +190,22 @@ class AppAssertionCredentials(AssertionCredentials):
"""
try:
scopes = self.scope.split()
(token, _) = app_identity.get_access_token(scopes)
except app_identity.Error, e:
(token, _) = app_identity.get_access_token(
scopes, service_account_id=self.service_account_id)
except app_identity.Error as e:
raise AccessTokenRefreshError(str(e))
self.access_token = token
@property
def serialization_data(self):
raise NotImplementedError('Cannot serialize credentials for AppEngine.')
def create_scoped_required(self):
return not self.scope
def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self._kwargs)
class FlowProperty(db.Property):
"""App Engine datastore Property for Flow.
......@@ -434,6 +449,7 @@ class StorageByKeyName(Storage):
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.
......@@ -456,6 +472,7 @@ class StorageByKeyName(Storage):
credentials.set_store(self)
return credentials
@db.non_transactional(allow_existing=True)
def locked_put(self, credentials):
"""Write a Credentials to the datastore.
......@@ -468,6 +485,7 @@ class StorageByKeyName(Storage):
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."""
......@@ -553,16 +571,14 @@ class OAuth2Decorator(object):
Instantiate and then use with oauth_required or oauth_aware
as decorators on webapp.RequestHandler methods.
Example:
::
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()
......@@ -650,8 +666,9 @@ class OAuth2Decorator(object):
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 be passed along as kwargs to the
OAuth2WebServerFlow constructor.
**kwargs: dict, Keyword arguments are passed along as kwargs to
the OAuth2WebServerFlow constructor.
"""
self._tls = threading.local()
self.flow = None
......@@ -798,14 +815,18 @@ class OAuth2Decorator(object):
url = self.flow.step1_get_authorize_url()
return str(url)
def http(self):
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(httplib2.Http())
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
@property
def callback_path(self):
......@@ -824,7 +845,8 @@ class OAuth2Decorator(object):
def callback_handler(self):
"""RequestHandler for the OAuth 2.0 redirect callback.
Usage:
Usage::
app = webapp.WSGIApplication([
('/index', MyIndexHandler),
...,
......@@ -858,7 +880,7 @@ class OAuth2Decorator(object):
user)
if decorator._token_response_param and credentials.token_response:
resp_json = simplejson.dumps(credentials.token_response)
resp_json = json.dumps(credentials.token_response)
redirect_uri = util._add_query_parameter(
redirect_uri, decorator._token_response_param, resp_json)
......@@ -887,24 +909,23 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
Uses a clientsecrets file as the source for all the information when
constructing an OAuth2Decorator.
Example:
::
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
"""
@util.positional(3)
def __init__(self, filename, scope, message=None, cache=None):
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
"""Constructor
Args:
......@@ -917,17 +938,20 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
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 InvalidClientSecretsError(
'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
constructor_kwargs = {
"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
......
# Copyright (C) 2010 Google Inc.
# 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.
......@@ -20,44 +20,48 @@ Tools for interacting with OAuth 2.0 protected resources.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import base64
import clientsecrets
import collections
import copy
import datetime
from .. import httplib2
import json
import logging
import os
import socket
import sys
import tempfile
import time
import urllib
import urlparse
import shutil
from .. import httplib2
from . import clientsecrets
from . import GOOGLE_AUTH_URI
from . import GOOGLE_DEVICE_URI
from . import GOOGLE_REVOKE_URI
from . import GOOGLE_TOKEN_URI
from . import util
from .anyjson import simplejson
from third_party import six
from third_party.six.moves import urllib
HAS_OPENSSL = False
HAS_CRYPTO = False
try:
from . import crypt
from oauth2client import crypt
HAS_CRYPTO = True
if crypt.OpenSSLVerifier is not None:
HAS_OPENSSL = True
except ImportError:
pass
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
logger = logging.getLogger(__name__)
# Expiry is stored in RFC3339 UTC format
EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
# Which certs to use to validate id_tokens received.
ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
# This symbol previously had a typo in the name; we keep the old name
# around for now, but will remove it in the future.
ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
# Constant to use for the out of band OAuth 2.0 flow.
OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
......@@ -65,6 +69,42 @@ OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
# Google Data client libraries may need to set this to [401, 403].
REFRESH_STATUS_CODES = [401]
# The value representing user credentials.
AUTHORIZED_USER = 'authorized_user'
# The value representing service account credentials.
SERVICE_ACCOUNT = 'service_account'
# The environment variable pointing the file with local
# Application Default Credentials.
GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
# The ~/.config subdirectory containing gcloud credentials. Intended
# to be swapped out in tests.
_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
# The error message we show users when we can't find the Application
# Default Credentials.
ADC_HELP_MSG = (
'The Application Default Credentials are not available. They are available '
'if running in Google Compute Engine. Otherwise, the environment variable '
+ GOOGLE_APPLICATION_CREDENTIALS +
' must be defined pointing to a file defining the credentials. See '
'https://developers.google.com/accounts/docs/application-default-credentials' # pylint:disable=line-too-long
' for more information.')
# The access token along with the seconds in which it expires.
AccessTokenInfo = collections.namedtuple(
'AccessTokenInfo', ['access_token', 'expires_in'])
DEFAULT_ENV_NAME = 'UNKNOWN'
# If set to True _get_environment avoid GCE check (_detect_gce_environment)
NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False')
class SETTINGS(object):
"""Settings namespace for globally defined values."""
env_name = None
class Error(Exception):
"""Base error for this module."""
......@@ -91,13 +131,25 @@ class AccessTokenCredentialsError(Error):
class VerifyJwtTokenError(Error):
"""Could on retrieve certificates for validation."""
"""Could not retrieve certificates for validation."""
class NonAsciiHeaderError(Error):
"""Header names and values must be ASCII strings."""
class ApplicationDefaultCredentialsError(Error):
"""Error retrieving the Application Default Credentials."""
class OAuth2DeviceCodeError(Error):
"""Error trying to retrieve a device code."""
class CryptoUnavailableError(Error, NotImplementedError):
"""Raised when a crypto library is required, but none is available."""
def _abstract():
raise NotImplementedError('You need to override this function')
......@@ -125,11 +177,12 @@ class Credentials(object):
an HTTP transport.
Subclasses must also specify a classmethod named 'from_json' that takes a JSON
string as input and returns an instaniated Credentials object.
string as input and returns an instantiated Credentials object.
"""
NON_SERIALIZED_MEMBERS = ['store']
def authorize(self, http):
"""Take an httplib2.Http instance (or equivalent) and authorizes it.
......@@ -143,6 +196,7 @@ class Credentials(object):
"""
_abstract()
def refresh(self, http):
"""Forces a refresh of the access_token.
......@@ -152,6 +206,7 @@ class Credentials(object):
"""
_abstract()
def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void.
......@@ -161,6 +216,7 @@ class Credentials(object):
"""
_abstract()
def apply(self, headers):
"""Add the authorization to the headers.
......@@ -184,12 +240,16 @@ class Credentials(object):
for member in strip:
if member in d:
del d[member]
if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
if (d.get('token_expiry') and
isinstance(d['token_expiry'], datetime.datetime)):
d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
# Add in information we will need later to reconsistitue this instance.
d['_class'] = t.__name__
d['_module'] = t.__module__
return simplejson.dumps(d)
for key, val in d.items():
if isinstance(val, bytes):
d[key] = val.decode('utf-8')
return json.dumps(d)
def to_json(self):
"""Creating a JSON representation of an instance of Credentials.
......@@ -212,14 +272,16 @@ class Credentials(object):
An instance of the subclass of Credentials that was serialized with
to_json().
"""
data = simplejson.loads(s)
if six.PY3 and isinstance(s, bytes):
s = s.decode('utf-8')
data = json.loads(s)
# Find and call the right classmethod from_json() to restore the object.
module = data['_module']
try:
m = __import__(module)
except ImportError:
# In case there's an object from the old package structure, update it
module = module.replace('.apiclient', '')
module = module.replace('.googleapiclient', '')
m = __import__(module)
m = __import__(module, fromlist=module.split('.')[:-1])
......@@ -228,13 +290,13 @@ class Credentials(object):
return from_json(s)
@classmethod
def from_json(cls, s):
def from_json(cls, unused_data):
"""Instantiate a Credentials object from a JSON description of it.
The JSON should have been produced by calling .to_json() on the object.
Args:
data: dict, A deserialized JSON object.
unused_data: dict, A deserialized JSON object.
Returns:
An instance of a Credentials subclass.
......@@ -356,8 +418,10 @@ def clean_headers(headers):
"""
clean = {}
try:
for k, v in headers.iteritems():
clean[str(k)] = str(v)
for k, v in six.iteritems(headers):
clean_k = k if isinstance(k, bytes) else str(k).encode('ascii')
clean_v = v if isinstance(v, bytes) else str(v).encode('ascii')
clean[clean_k] = clean_v
except UnicodeEncodeError:
raise NonAsciiHeaderError(k + ': ' + v)
return clean
......@@ -373,11 +437,11 @@ def _update_query_params(uri, params):
Returns:
The same URI but with the new query parameters added.
"""
parts = list(urlparse.urlparse(uri))
query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
parts = urllib.parse.urlparse(uri)
query_params = dict(urllib.parse.parse_qsl(parts.query))
query_params.update(params)
parts[4] = urllib.urlencode(query_params)
return urlparse.urlunparse(parts)
new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
return urllib.parse.urlunparse(new_parts)
class OAuth2Credentials(Credentials):
......@@ -445,22 +509,23 @@ class OAuth2Credentials(Credentials):
it.
Args:
http: An instance of httplib2.Http
or something that acts like it.
http: An instance of ``httplib2.Http`` or something that acts
like it.
Returns:
A modified instance of http that was passed in.
Example:
Example::
h = httplib2.Http()
h = credentials.authorize(h)
You can't create a new OAuth subclass of httplib2.Authenication
You can't create a new OAuth subclass of httplib2.Authentication
because it never gets passed the absolute URI, which is needed for
signing. So instead we have to overload 'request' with a closure
that adds in the Authorization header and then calls the original
version of 'request()'.
"""
request_orig = http.request
......@@ -473,10 +538,12 @@ class OAuth2Credentials(Credentials):
logger.info('Attempting refresh to obtain initial access_token')
self._refresh(request_orig)
# Modify the request headers to add the appropriate
# Clone and modify the request headers to add the appropriate
# Authorization header.
if headers is None:
headers = {}
else:
headers = dict(headers)
self.apply(headers)
if self.user_agent is not None:
......@@ -489,7 +556,7 @@ class OAuth2Credentials(Credentials):
redirections, connection_type)
if resp.status in REFRESH_STATUS_CODES:
logger.info('Refreshing due to a %s' % str(resp.status))
logger.info('Refreshing due to a %s', resp.status)
self._refresh(request_orig)
self.apply(headers)
return request_orig(uri, method, body, clean_headers(headers),
......@@ -545,13 +612,15 @@ class OAuth2Credentials(Credentials):
Returns:
An instance of a Credentials subclass.
"""
data = simplejson.loads(s)
if 'token_expiry' in data and not isinstance(data['token_expiry'],
datetime.datetime):
if six.PY3 and isinstance(s, bytes):
s = s.decode('utf-8')
data = json.loads(s)
if (data.get('token_expiry') and
not isinstance(data['token_expiry'], datetime.datetime)):
try:
data['token_expiry'] = datetime.datetime.strptime(
data['token_expiry'], EXPIRY_FORMAT)
except:
except ValueError:
data['token_expiry'] = None
retval = cls(
data['access_token'],
......@@ -586,11 +655,24 @@ class OAuth2Credentials(Credentials):
return True
return False
def get_access_token(self, http=None):
"""Return the access token and its expiration information.
If the token does not exist, get one.
If the token expired, refresh it.
"""
if not self.access_token or self.access_token_expired:
if not http:
http = httplib2.Http()
self.refresh(http)
return AccessTokenInfo(access_token=self.access_token,
expires_in=self._expires_in())
def set_store(self, store):
"""Set the Storage for the credential.
Args:
store: Storage, an implementation of Stroage object.
store: Storage, an implementation of Storage object.
This is needed to store the latest access_token if it
has expired and been refreshed. This implementation uses
locking to check for updates before updating the
......@@ -598,6 +680,25 @@ class OAuth2Credentials(Credentials):
"""
self.store = store
def _expires_in(self):
"""Return the number of seconds until this token expires.
If token_expiry is in the past, this method will return 0, meaning the
token has already expired.
If token_expiry is None, this method will return None. Note that returning
0 in such a case would not be fair: the token may still be valid;
we just don't know anything about it.
"""
if self.token_expiry:
now = datetime.datetime.utcnow()
if self.token_expiry > now:
time_delta = self.token_expiry - now
# TODO(orestica): return time_delta.total_seconds()
# once dropping support for Python 2.6
return time_delta.days * 86400 + time_delta.seconds
else:
return 0
def _updateFromCredential(self, other):
"""Update this Credential from another instance."""
self.__dict__.update(other.__getstate__())
......@@ -615,7 +716,7 @@ class OAuth2Credentials(Credentials):
def _generate_refresh_request_body(self):
"""Generate the body that will be used in the refresh request."""
body = urllib.urlencode({
body = urllib.parse.urlencode({
'grant_type': 'refresh_token',
'client_id': self.client_id,
'client_secret': self.client_secret,
......@@ -679,9 +780,10 @@ class OAuth2Credentials(Credentials):
logger.info('Refreshing access_token')
resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers)
if six.PY3 and isinstance(content, bytes):
content = content.decode('utf-8')
if resp.status == 200:
# TODO(jcgregorio) Raise an error if loads fails?
d = simplejson.loads(content)
d = json.loads(content)
self.token_response = d
self.access_token = d['access_token']
self.refresh_token = d.get('refresh_token', self.refresh_token)
......@@ -690,35 +792,40 @@ class OAuth2Credentials(Credentials):
seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
else:
self.token_expiry = None
# On temporary refresh errors, the user does not actually have to
# re-authorize, so we unflag here.
self.invalid = False
if self.store:
self.store.locked_put(self)
else:
# An {'error':...} response body means the token is expired or revoked,
# so we flag the credentials as such.
logger.info('Failed to retrieve access token: %s' % content)
logger.info('Failed to retrieve access token: %s', content)
error_msg = 'Invalid response %s.' % resp['status']
try:
d = simplejson.loads(content)
d = json.loads(content)
if 'error' in d:
error_msg = d['error']
if 'error_description' in d:
error_msg += ': ' + d['error_description']
self.invalid = True
if self.store:
self.store.locked_put(self)
except StandardError:
except (TypeError, ValueError):
pass
raise AccessTokenRefreshError(error_msg)
def _revoke(self, http_request):
"""Revokes the refresh_token and deletes the store if available.
"""Revokes this credential and deletes the stored copy (if it exists).
Args:
http_request: callable, a callable that matches the method signature of
httplib2.Http.request, used to make the revoke request.
"""
self._do_revoke(http_request, self.refresh_token)
self._do_revoke(http_request, self.refresh_token or self.access_token)
def _do_revoke(self, http_request, token):
"""Revokes the credentials and deletes the store if available.
"""Revokes this credential and deletes the stored copy (if it exists).
Args:
http_request: callable, a callable that matches the method signature of
......@@ -738,10 +845,10 @@ class OAuth2Credentials(Credentials):
else:
error_msg = 'Invalid response %s.' % resp.status
try:
d = simplejson.loads(content)
d = json.loads(content)
if 'error' in d:
error_msg = d['error']
except StandardError:
except (TypeError, ValueError):
pass
raise TokenRevokeError(error_msg)
......@@ -763,7 +870,8 @@ class AccessTokenCredentials(OAuth2Credentials):
AccessTokenCredentials objects may be safely pickled and unpickled.
Usage:
Usage::
credentials = AccessTokenCredentials('<an access token>',
'my-user-agent/1.0')
http = httplib2.Http()
......@@ -799,7 +907,9 @@ class AccessTokenCredentials(OAuth2Credentials):
@classmethod
def from_json(cls, s):
data = simplejson.loads(s)
if six.PY3 and isinstance(s, bytes):
s = s.decode('utf-8')
data = json.loads(s)
retval = AccessTokenCredentials(
data['access_token'],
data['user_agent'])
......@@ -819,7 +929,434 @@ class AccessTokenCredentials(OAuth2Credentials):
self._do_revoke(http_request, self.access_token)
class AssertionCredentials(OAuth2Credentials):
def _detect_gce_environment(urlopen=None):
"""Determine if the current environment is Compute Engine.
Args:
urlopen: Optional argument. Function used to open a connection to a URL.
Returns:
Boolean indicating whether or not the current environment is Google
Compute Engine.
"""
urlopen = urlopen or urllib.request.urlopen
# Note: the explicit `timeout` below is a workaround. The underlying
# issue is that resolving an unknown host on some networks will take
# 20-30 seconds; making this timeout short fixes the issue, 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
# "unlikely".
try:
response = urlopen('http://169.254.169.254/', timeout=1)
return response.info().get('Metadata-Flavor', '') == 'Google'
except socket.timeout:
logger.info('Timeout attempting to reach GCE metadata service.')
return False
except urllib.error.URLError as e:
if isinstance(getattr(e, 'reason', None), socket.timeout):
logger.info('Timeout attempting to reach GCE metadata service.')
return False
def _get_environment(urlopen=None):
"""Detect the environment the code is being run on.
Args:
urlopen: Optional argument. Function used to open a connection to a URL.
Returns:
The value of SETTINGS.env_name after being set. If already
set, simply returns the value.
"""
if SETTINGS.env_name is not None:
return SETTINGS.env_name
# None is an unset value, not the default.
SETTINGS.env_name = DEFAULT_ENV_NAME
server_software = os.environ.get('SERVER_SOFTWARE', '')
if server_software.startswith('Google App Engine/'):
SETTINGS.env_name = 'GAE_PRODUCTION'
elif server_software.startswith('Development/'):
SETTINGS.env_name = 'GAE_LOCAL'
elif NO_GCE_CHECK != 'True' and _detect_gce_environment(urlopen=urlopen):
SETTINGS.env_name = 'GCE_PRODUCTION'
return SETTINGS.env_name
class GoogleCredentials(OAuth2Credentials):
"""Application Default Credentials for use in calling Google APIs.
The Application Default Credentials are being constructed as a function of
the environment where the code is being run.
More details can be found on this page:
https://developers.google.com/accounts/docs/application-default-credentials
Here is an example of how to use the Application Default Credentials for a
service that requires authentication:
from googleapiclient.discovery import build
from oauth2client.client import GoogleCredentials
credentials = GoogleCredentials.get_application_default()
service = build('compute', 'v1', credentials=credentials)
PROJECT = 'bamboo-machine-422'
ZONE = 'us-central1-a'
request = service.instances().list(project=PROJECT, zone=ZONE)
response = request.execute()
print(response)
"""
def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent,
revoke_uri=GOOGLE_REVOKE_URI):
"""Create an instance of GoogleCredentials.
This constructor is not usually called by the user, instead
GoogleCredentials objects are instantiated by
GoogleCredentials.from_stream() or
GoogleCredentials.get_application_default().
Args:
access_token: string, access token.
client_id: string, client identifier.
client_secret: string, client secret.
refresh_token: string, refresh token.
token_expiry: datetime, when the access_token expires.
token_uri: string, URI of token endpoint.
user_agent: string, The HTTP User-Agent to provide for this application.
revoke_uri: string, URI for revoke endpoint.
Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None.
"""
super(GoogleCredentials, self).__init__(
access_token, client_id, client_secret, refresh_token, token_expiry,
token_uri, user_agent, revoke_uri=revoke_uri)
def create_scoped_required(self):
"""Whether this Credentials object is scopeless.
create_scoped(scopes) method needs to be called in order to create
a Credentials object for API calls.
"""
return False
def create_scoped(self, scopes):
"""Create a Credentials object for the given scopes.
The Credentials type is preserved.
"""
return self
@property
def serialization_data(self):
"""Get the fields and their values identifying the current credentials."""
return {
'type': 'authorized_user',
'client_id': self.client_id,
'client_secret': self.client_secret,
'refresh_token': self.refresh_token
}
@staticmethod
def _implicit_credentials_from_gae(env_name=None):
"""Attempts to get implicit credentials in Google App Engine env.
If the current environment is not detected as App Engine, returns None,
indicating no Google App Engine credentials can be detected from the
current environment.
Args:
env_name: String, indicating current environment.
Returns:
None, if not in GAE, else an appengine.AppAssertionCredentials object.
"""
env_name = env_name or _get_environment()
if env_name not in ('GAE_PRODUCTION', 'GAE_LOCAL'):
return None
return _get_application_default_credential_GAE()
@staticmethod
def _implicit_credentials_from_gce(env_name=None):
"""Attempts to get implicit credentials in Google Compute Engine env.
If the current environment is not detected as Compute Engine, returns None,
indicating no Google Compute Engine credentials can be detected from the
current environment.
Args:
env_name: String, indicating current environment.
Returns:
None, if not in GCE, else a gce.AppAssertionCredentials object.
"""
env_name = env_name or _get_environment()
if env_name != 'GCE_PRODUCTION':
return None
return _get_application_default_credential_GCE()
@staticmethod
def _implicit_credentials_from_files(env_name=None):
"""Attempts to get implicit credentials from local credential files.
First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS
is set with a filename and then falls back to a configuration file (the
"well known" file) associated with the 'gcloud' command line tool.
Args:
env_name: Unused argument.
Returns:
Credentials object associated with the GOOGLE_APPLICATION_CREDENTIALS
file or the "well known" file if either exist. If neither file is
define, returns None, indicating no credentials from a file can
detected from the current environment.
"""
credentials_filename = _get_environment_variable_file()
if not credentials_filename:
credentials_filename = _get_well_known_file()
if os.path.isfile(credentials_filename):
extra_help = (' (produced automatically when running'
' "gcloud auth login" command)')
else:
credentials_filename = None
else:
extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS +
' environment variable)')
if not credentials_filename:
return
try:
return _get_application_default_credential_from_file(credentials_filename)
except (ApplicationDefaultCredentialsError, ValueError) as error:
_raise_exception_for_reading_json(credentials_filename, extra_help, error)
@classmethod
def _get_implicit_credentials(cls):
"""Gets credentials implicitly from the environment.
Checks environment in order of precedence:
- Google App Engine (production and testing)
- Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to
a file with stored credentials information.
- Stored "well known" file associated with `gcloud` command line tool.
- Google Compute Engine production environment.
Exceptions:
ApplicationDefaultCredentialsError: raised when the credentials fail
to be retrieved.
"""
env_name = _get_environment()
# Environ checks (in order). Assumes each checker takes `env_name`
# as a kwarg.
environ_checkers = [
cls._implicit_credentials_from_gae,
cls._implicit_credentials_from_files,
cls._implicit_credentials_from_gce,
]
for checker in environ_checkers:
credentials = checker(env_name=env_name)
if credentials is not None:
return credentials
# If no credentials, fail.
raise ApplicationDefaultCredentialsError(ADC_HELP_MSG)
@staticmethod
def get_application_default():
"""Get the Application Default Credentials for the current environment.
Exceptions:
ApplicationDefaultCredentialsError: raised when the credentials fail
to be retrieved.
"""
return GoogleCredentials._get_implicit_credentials()
@staticmethod
def from_stream(credential_filename):
"""Create a Credentials object by reading the information from a given file.
It returns an object of type GoogleCredentials.
Args:
credential_filename: the path to the file from where the credentials
are to be read
Exceptions:
ApplicationDefaultCredentialsError: raised when the credentials fail
to be retrieved.
"""
if credential_filename and os.path.isfile(credential_filename):
try:
return _get_application_default_credential_from_file(
credential_filename)
except (ApplicationDefaultCredentialsError, ValueError) as error:
extra_help = ' (provided as parameter to the from_stream() method)'
_raise_exception_for_reading_json(credential_filename,
extra_help,
error)
else:
raise ApplicationDefaultCredentialsError(
'The parameter passed to the from_stream() '
'method should point to a file.')
def _save_private_file(filename, json_contents):
"""Saves a file with read-write permissions on for the owner.
Args:
filename: String. Absolute path to file.
json_contents: JSON serializable object to be saved.
"""
temp_filename = tempfile.mktemp()
file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600)
with os.fdopen(file_desc, 'w') as file_handle:
json.dump(json_contents, file_handle, sort_keys=True,
indent=2, separators=(',', ': '))
shutil.move(temp_filename, filename)
def save_to_well_known_file(credentials, well_known_file=None):
"""Save the provided GoogleCredentials to the well known file.
Args:
credentials:
the credentials to be saved to the well known file;
it should be an instance of GoogleCredentials
well_known_file:
the name of the file where the credentials are to be saved;
this parameter is supposed to be used for testing only
"""
# TODO(orestica): move this method to tools.py
# once the argparse import gets fixed (it is not present in Python 2.6)
if well_known_file is None:
well_known_file = _get_well_known_file()
credentials_data = credentials.serialization_data
_save_private_file(well_known_file, credentials_data)
def _get_environment_variable_file():
application_default_credential_filename = (
os.environ.get(GOOGLE_APPLICATION_CREDENTIALS,
None))
if application_default_credential_filename:
if os.path.isfile(application_default_credential_filename):
return application_default_credential_filename
else:
raise ApplicationDefaultCredentialsError(
'File ' + application_default_credential_filename + ' (pointed by ' +
GOOGLE_APPLICATION_CREDENTIALS +
' environment variable) does not exist!')
def _get_well_known_file():
"""Get the well known file produced by command 'gcloud auth login'."""
# TODO(orestica): Revisit this method once gcloud provides a better way
# of pinpointing the exact location of the file.
WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
if os.name == 'nt':
try:
default_config_path = os.path.join(os.environ['APPDATA'],
_CLOUDSDK_CONFIG_DIRECTORY)
except KeyError:
# This should never happen unless someone is really messing with things.
drive = os.environ.get('SystemDrive', 'C:')
default_config_path = os.path.join(drive, '\\',
_CLOUDSDK_CONFIG_DIRECTORY)
else:
default_config_path = os.path.join(os.path.expanduser('~'),
'.config',
_CLOUDSDK_CONFIG_DIRECTORY)
default_config_path = os.path.join(default_config_path,
WELL_KNOWN_CREDENTIALS_FILE)
return default_config_path
def _get_application_default_credential_from_file(filename):
"""Build the Application Default Credentials from file."""
from oauth2client import service_account
# read the credentials from the file
with open(filename) as file_obj:
client_credentials = json.load(file_obj)
credentials_type = client_credentials.get('type')
if credentials_type == AUTHORIZED_USER:
required_fields = set(['client_id', 'client_secret', 'refresh_token'])
elif credentials_type == SERVICE_ACCOUNT:
required_fields = set(['client_id', 'client_email', 'private_key_id',
'private_key'])
else:
raise ApplicationDefaultCredentialsError(
"'type' field should be defined (and have one of the '" +
AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
missing_fields = required_fields.difference(client_credentials.keys())
if missing_fields:
_raise_exception_for_missing_fields(missing_fields)
if client_credentials['type'] == AUTHORIZED_USER:
return GoogleCredentials(
access_token=None,
client_id=client_credentials['client_id'],
client_secret=client_credentials['client_secret'],
refresh_token=client_credentials['refresh_token'],
token_expiry=None,
token_uri=GOOGLE_TOKEN_URI,
user_agent='Python client library')
else: # client_credentials['type'] == SERVICE_ACCOUNT
return service_account._ServiceAccountCredentials(
service_account_id=client_credentials['client_id'],
service_account_email=client_credentials['client_email'],
private_key_id=client_credentials['private_key_id'],
private_key_pkcs8_text=client_credentials['private_key'],
scopes=[])
def _raise_exception_for_missing_fields(missing_fields):
raise ApplicationDefaultCredentialsError(
'The following field(s) must be defined: ' + ', '.join(missing_fields))
def _raise_exception_for_reading_json(credential_file,
extra_help,
error):
raise ApplicationDefaultCredentialsError(
'An error was encountered while reading json file: '+
credential_file + extra_help + ': ' + str(error))
def _get_application_default_credential_GAE():
from oauth2client.appengine import AppAssertionCredentials
return AppAssertionCredentials([])
def _get_application_default_credential_GCE():
from oauth2client.gce import AppAssertionCredentials
return AppAssertionCredentials([])
class AssertionCredentials(GoogleCredentials):
"""Abstract Credentials object used for OAuth 2.0 assertion grants.
This credential does not require a flow to instantiate because it
......@@ -859,7 +1396,7 @@ class AssertionCredentials(OAuth2Credentials):
def _generate_refresh_request_body(self):
assertion = self._generate_assertion()
body = urllib.urlencode({
body = urllib.parse.urlencode({
'assertion': assertion,
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
})
......@@ -882,20 +1419,27 @@ class AssertionCredentials(OAuth2Credentials):
self._do_revoke(http_request, self.access_token)
if HAS_CRYPTO:
# PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is
# missing then don't create the SignedJwtAssertionCredentials or the
# verify_id_token() method.
def _RequireCryptoOrDie():
"""Ensure we have a crypto library, or throw CryptoUnavailableError.
The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
to be available in order to function, but these are optional
dependencies.
"""
if not HAS_CRYPTO:
raise CryptoUnavailableError('No crypto library available')
class SignedJwtAssertionCredentials(AssertionCredentials):
class SignedJwtAssertionCredentials(AssertionCredentials):
"""Credentials object used for OAuth 2.0 Signed JWT assertion grants.
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.
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.
SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or
later. For App Engine you may also consider using AppAssertionCredentials.
SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
2.6 or later. For App Engine you may also consider using
AppAssertionCredentials.
"""
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
......@@ -924,8 +1468,12 @@ if HAS_CRYPTO:
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
revoke_uri: string, URI for revoke endpoint.
kwargs: kwargs, Additional parameters to add to the JWT token, for
example sub=joe@xample.org."""
example sub=joe@xample.org.
Raises:
CryptoUnavailableError if no crypto library is available.
"""
_RequireCryptoOrDie()
super(SignedJwtAssertionCredentials, self).__init__(
None,
user_agent=user_agent,
......@@ -937,6 +1485,8 @@ if HAS_CRYPTO:
# Keep base64 encoded so it can be stored in JSON.
self.private_key = base64.b64encode(private_key)
if isinstance(self.private_key, six.text_type):
self.private_key = self.private_key.encode('utf-8')
self.private_key_password = private_key_password
self.service_account_name = service_account_name
......@@ -944,7 +1494,7 @@ if HAS_CRYPTO:
@classmethod
def from_json(cls, s):
data = simplejson.loads(s)
data = json.loads(s)
retval = SignedJwtAssertionCredentials(
data['service_account_name'],
base64.b64decode(data['private_key']),
......@@ -960,7 +1510,7 @@ if HAS_CRYPTO:
def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""
now = long(time.time())
now = int(time.time())
payload = {
'aud': self.token_uri,
'scope': self.scope,
......@@ -975,13 +1525,13 @@ if HAS_CRYPTO:
return crypt.make_signed_jwt(crypt.Signer.from_string(
private_key, self.private_key_password), payload)
# Only used in verify_id_token(), which is always calling to the same URI
# for the certs.
_cached_http = httplib2.Http(MemoryCache())
# Only used in verify_id_token(), which is always calling to the same URI
# for the certs.
_cached_http = httplib2.Http(MemoryCache())
@util.positional(2)
def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATON_CERTS):
@util.positional(2)
def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATION_CERTS):
"""Verifies a signed JWT id_token.
This function requires PyOpenSSL and because of that it does not work on
......@@ -999,15 +1549,17 @@ if HAS_CRYPTO:
The deserialized JSON in the JWT.
Raises:
oauth2client.crypt.AppIdentityError if the JWT fails to verify.
oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
CryptoUnavailableError: if no crypto library is available.
"""
_RequireCryptoOrDie()
if http is None:
http = _cached_http
resp, content = http.request(cert_uri)
if resp.status == 200:
certs = simplejson.loads(content)
certs = json.loads(content.decode('utf-8'))
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
else:
raise VerifyJwtTokenError('Status code: %d' % resp.status)
......@@ -1015,8 +1567,9 @@ if HAS_CRYPTO:
def _urlsafe_b64decode(b64string):
# Guard against unicode strings, which base64 can't handle.
if isinstance(b64string, six.text_type):
b64string = b64string.encode('ascii')
padded = b64string + '=' * (4 - len(b64string) % 4)
padded = b64string + b'=' * (4 - len(b64string) % 4)
return base64.urlsafe_b64decode(padded)
......@@ -1026,18 +1579,21 @@ def _extract_id_token(id_token):
Does the extraction w/o checking the signature.
Args:
id_token: string, OAuth 2.0 id_token.
id_token: string or bytestring, OAuth 2.0 id_token.
Returns:
object, The deserialized JSON payload.
"""
segments = id_token.split('.')
if type(id_token) == bytes:
segments = id_token.split(b'.')
else:
segments = id_token.split(u'.')
if (len(segments) != 3):
if len(segments) != 3:
raise VerifyJwtTokenError(
'Wrong number of segments in token: %s' % id_token)
return simplejson.loads(_urlsafe_b64decode(segments[1]))
return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8'))
def _parse_exchange_token_response(content):
......@@ -1055,11 +1611,12 @@ def _parse_exchange_token_response(content):
"""
resp = {}
try:
resp = simplejson.loads(content)
except StandardError:
resp = json.loads(content.decode('utf-8'))
except Exception:
# different JSON libs raise different exceptions,
# so we just do a catch-all here
resp = dict(parse_qsl(content))
content = content.decode('utf-8')
resp = dict(urllib.parse.parse_qsl(content))
# some providers respond with 'expires', others with 'expires_in'
if resp and 'expires' in resp:
......@@ -1073,14 +1630,15 @@ def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None,
user_agent=None, token_uri=GOOGLE_TOKEN_URI,
auth_uri=GOOGLE_AUTH_URI,
revoke_uri=GOOGLE_REVOKE_URI):
revoke_uri=GOOGLE_REVOKE_URI,
device_uri=GOOGLE_DEVICE_URI):
"""Exchanges an authorization code for an OAuth2Credentials object.
Args:
client_id: string, client identifier.
client_secret: string, client secret.
scope: string or iterable of strings, scope(s) to request.
code: string, An authroization code, most likely passed down from
code: string, An authorization code, most likely passed down from
the client
redirect_uri: string, this is generally set to 'postmessage' to match the
redirect_uri that the client specified
......@@ -1091,6 +1649,8 @@ def credentials_from_code(client_id, client_secret, scope, code,
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.
device_uri: string, URI for device authorization endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Returns:
An OAuth2Credentials object.
......@@ -1102,7 +1662,7 @@ def credentials_from_code(client_id, client_secret, scope, code,
flow = OAuth2WebServerFlow(client_id, client_secret, scope,
redirect_uri=redirect_uri, user_agent=user_agent,
auth_uri=auth_uri, token_uri=token_uri,
revoke_uri=revoke_uri)
revoke_uri=revoke_uri, device_uri=device_uri)
credentials = flow.step2_exchange(code, http=http)
return credentials
......@@ -1113,7 +1673,8 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
message = None,
redirect_uri='postmessage',
http=None,
cache=None):
cache=None,
device_uri=None):
"""Returns OAuth2Credentials from a clientsecrets file and an auth code.
Will create the right kind of Flow based on the contents of the clientsecrets
......@@ -1133,6 +1694,7 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
http: httplib2.Http, optional http instance to use to do the fetch
cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
device_uri: string, OAuth 2.0 device authorization endpoint
Returns:
An OAuth2Credentials object.
......@@ -1145,11 +1707,49 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
invalid.
"""
flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
redirect_uri=redirect_uri)
redirect_uri=redirect_uri,
device_uri=device_uri)
credentials = flow.step2_exchange(code, http=http)
return credentials
class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
'device_code', 'user_code', 'interval', 'verification_url',
'user_code_expiry'))):
"""Intermediate information the OAuth2 for devices flow."""
@classmethod
def FromResponse(cls, response):
"""Create a DeviceFlowInfo from a server response.
The response should be a dict containing entries as described here:
http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1
"""
# device_code, user_code, and verification_url are required.
kwargs = {
'device_code': response['device_code'],
'user_code': response['user_code'],
}
# The response may list the verification address as either
# verification_url or verification_uri, so we check for both.
verification_url = response.get(
'verification_url', response.get('verification_uri'))
if verification_url is None:
raise OAuth2DeviceCodeError(
'No verification_url provided in server response')
kwargs['verification_url'] = verification_url
# expires_in and interval are optional.
kwargs.update({
'interval': response.get('interval'),
'user_code_expiry': None,
})
if 'expires_in' in response:
kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta(
seconds=int(response['expires_in']))
return cls(**kwargs)
class OAuth2WebServerFlow(Flow):
"""Does the Web Server Flow for OAuth 2.0.
......@@ -1163,6 +1763,8 @@ class OAuth2WebServerFlow(Flow):
auth_uri=GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
login_hint=None,
device_uri=GOOGLE_DEVICE_URI,
**kwargs):
"""Constructor for OAuth2WebServerFlow.
......@@ -1185,6 +1787,11 @@ class OAuth2WebServerFlow(Flow):
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.
login_hint: string, Either an email address or domain. Passing this hint
will either pre-fill the email box on the sign-in form or select the
proper multi-login session, thereby simplifying the login flow.
device_uri: string, URI for device authorization endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
**kwargs: dict, The keyword arguments are all optional and required
parameters for the OAuth calls.
"""
......@@ -1192,10 +1799,12 @@ class OAuth2WebServerFlow(Flow):
self.client_secret = client_secret
self.scope = util.scopes_to_string(scope)
self.redirect_uri = redirect_uri
self.login_hint = login_hint
self.user_agent = user_agent
self.auth_uri = auth_uri
self.token_uri = token_uri
self.revoke_uri = revoke_uri
self.device_uri = device_uri
self.params = {
'access_type': 'offline',
'response_type': 'code',
......@@ -1216,8 +1825,9 @@ class OAuth2WebServerFlow(Flow):
A URI as a string to redirect the user to begin the authorization flow.
"""
if redirect_uri is not None:
logger.warning(('The redirect_uri parameter for'
'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
logger.warning((
'The redirect_uri parameter for '
'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please '
'move to passing the redirect_uri in via the constructor.'))
self.redirect_uri = redirect_uri
......@@ -1229,45 +1839,107 @@ class OAuth2WebServerFlow(Flow):
'redirect_uri': self.redirect_uri,
'scope': self.scope,
}
if self.login_hint is not None:
query_params['login_hint'] = self.login_hint
query_params.update(self.params)
return _update_query_params(self.auth_uri, query_params)
@util.positional(1)
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 as a string for the user to authorize the application
An URL as a string where the user has to enter the code
"""
if self.device_uri is None:
raise ValueError('The value of device_uri must not be None.')
body = urllib.parse.urlencode({
'client_id': self.client_id,
'scope': self.scope,
})
headers = {
'content-type': 'application/x-www-form-urlencoded',
}
if self.user_agent is not None:
headers['user-agent'] = self.user_agent
if http is None:
http = httplib2.Http()
resp, content = http.request(self.device_uri, method='POST', body=body,
headers=headers)
if resp.status == 200:
try:
flow_info = json.loads(content)
except ValueError as e:
raise OAuth2DeviceCodeError(
'Could not parse server response as JSON: "%s", error: "%s"' % (
content, e))
return DeviceFlowInfo.FromResponse(flow_info)
else:
error_msg = 'Invalid response %s.' % resp.status
try:
d = json.loads(content)
if 'error' in d:
error_msg += ' Error: %s' % d['error']
except ValueError:
# Couldn't decode a JSON response, stick with the default message.
pass
raise OAuth2DeviceCodeError(error_msg)
@util.positional(2)
def step2_exchange(self, code, http=None):
"""Exhanges a code for OAuth2Credentials.
def step2_exchange(self, code=None, http=None, device_flow_info=None):
"""Exchanges a code for OAuth2Credentials.
Args:
code: string or dict, either the code as a string, or a dictionary
of the query parameters to the redirect_uri, which contains
the code.
http: httplib2.Http, optional http instance to use to do the fetch
code: string, a dict-like object, or None. For a non-device
flow, this is either the response code as a string, or a
dictionary of query parameters to the redirect_uri. For a
device flow, this should be None.
http: httplib2.Http, optional http instance to use when fetching
credentials.
device_flow_info: DeviceFlowInfo, return value from step1 in the
case of a device flow.
Returns:
An OAuth2Credentials object that can be used to authorize requests.
Raises:
FlowExchangeError if a problem occured exchanging the code for a
FlowExchangeError: if a problem occurred exchanging the code for a
refresh_token.
"""
ValueError: if code and device_flow_info are both provided or both
missing.
if not (isinstance(code, str) or isinstance(code, unicode)):
"""
if code is None and device_flow_info is None:
raise ValueError('No code or device_flow_info provided.')
if code is not None and device_flow_info is not None:
raise ValueError('Cannot provide both code and device_flow_info.')
if code is None:
code = device_flow_info.device_code
elif not isinstance(code, six.string_types):
if 'code' not in code:
if 'error' in code:
error_msg = code['error']
else:
error_msg = 'No code was supplied in the query parameters.'
raise FlowExchangeError(error_msg)
else:
raise FlowExchangeError(code.get(
'error', 'No code was supplied in the query parameters.'))
code = code['code']
body = urllib.urlencode({
'grant_type': 'authorization_code',
post_data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.redirect_uri,
'scope': self.scope,
})
}
if device_flow_info is not None:
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
else:
post_data['grant_type'] = 'authorization_code'
post_data['redirect_uri'] = self.redirect_uri
body = urllib.parse.urlencode(post_data)
headers = {
'content-type': 'application/x-www-form-urlencoded',
}
......@@ -1284,26 +1956,31 @@ class OAuth2WebServerFlow(Flow):
if resp.status == 200 and 'access_token' in d:
access_token = d['access_token']
refresh_token = d.get('refresh_token', None)
if not refresh_token:
logger.info(
'Received token response with no refresh_token. Consider '
"reauthenticating with approval_prompt='force'.")
token_expiry = None
if 'expires_in' in d:
token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
seconds=int(d['expires_in']))
extracted_id_token = None
if 'id_token' in d:
d['id_token'] = _extract_id_token(d['id_token'])
extracted_id_token = _extract_id_token(d['id_token'])
logger.info('Successfully retrieved access token')
return OAuth2Credentials(access_token, self.client_id,
self.client_secret, refresh_token, token_expiry,
self.token_uri, self.user_agent,
revoke_uri=self.revoke_uri,
id_token=d.get('id_token', None),
id_token=extracted_id_token,
token_response=d)
else:
logger.info('Failed to retrieve access token: %s' % content)
logger.info('Failed to retrieve access token: %s', content)
if 'error' in d:
# you never know what those providers got to say
error_msg = unicode(d['error'])
error_msg = str(d['error']) + str(d.get('error_description', ''))
else:
error_msg = 'Invalid response: %s.' % str(resp.status)
raise FlowExchangeError(error_msg)
......@@ -1311,7 +1988,8 @@ class OAuth2WebServerFlow(Flow):
@util.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None):
message=None, cache=None, login_hint=None,
device_uri=None):
"""Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the clientsecrets
......@@ -1329,6 +2007,11 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
provided then clientsecrets.InvalidClientSecretsError will be raised.
cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
login_hint: string, Either an email address or domain. Passing this hint
will either pre-fill the email box on the sign-in form or select the
proper multi-login session, thereby simplifying the login flow.
device_uri: string, URI for device authorization endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Returns:
A Flow object.
......@@ -1345,10 +2028,13 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
'redirect_uri': redirect_uri,
'auth_uri': client_info['auth_uri'],
'token_uri': client_info['token_uri'],
'login_hint': login_hint,
}
revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None:
constructor_kwargs['revoke_uri'] = revoke_uri
if device_uri is not None:
constructor_kwargs['device_uri'] = device_uri
return OAuth2WebServerFlow(
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)
......
# Copyright (C) 2011 Google Inc.
# 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.
......@@ -20,8 +20,10 @@ an OAuth 2.0 protected service.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import json
from third_party import six
from anyjson import simplejson
# Properties that make a client_secrets.json file valid.
TYPE_WEB = 'web'
......@@ -68,11 +70,21 @@ class InvalidClientSecretsError(Error):
def _validate_clientsecrets(obj):
if obj is None or len(obj) != 1:
raise InvalidClientSecretsError('Invalid file format.')
client_type = obj.keys()[0]
if client_type not in VALID_CLIENT.keys():
raise InvalidClientSecretsError('Unknown client type: %s.' % client_type)
_INVALID_FILE_FORMAT_MSG = (
'Invalid file format. See '
'https://developers.google.com/api-client-library/'
'python/guide/aaa_client_secrets')
if obj is None:
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
if len(obj) != 1:
raise InvalidClientSecretsError(
_INVALID_FILE_FORMAT_MSG + ' '
'Expected a JSON object with a single property for a "web" or '
'"installed" application')
client_type = tuple(obj)[0]
if client_type not in VALID_CLIENT:
raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type,))
client_info = obj[client_type]
for prop_name in VALID_CLIENT[client_type]['required']:
if prop_name not in client_info:
......@@ -87,22 +99,19 @@ def _validate_clientsecrets(obj):
def load(fp):
obj = simplejson.load(fp)
obj = json.load(fp)
return _validate_clientsecrets(obj)
def loads(s):
obj = simplejson.loads(s)
obj = json.loads(s)
return _validate_clientsecrets(obj)
def _loadfile(filename):
try:
fp = file(filename, 'r')
try:
obj = simplejson.load(fp)
finally:
fp.close()
with open(filename, 'r') as fp:
obj = json.load(fp)
except IOError:
raise InvalidClientSecretsError('File not found: "%s"' % filename)
return _validate_clientsecrets(obj)
......@@ -114,10 +123,12 @@ def loadfile(filename, cache=None):
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:
* ``get(key, namespace=ns)``
* ``set(key, value, namespace=ns)``
Usage::
# without caching
client_type, client_info = loadfile('secrets.json')
# using App Engine memcache service
......@@ -150,4 +161,4 @@ def loadfile(filename, cache=None):
obj = {client_type: client_info}
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
return obj.iteritems().next()
return next(six.iteritems(obj))
\ No newline at end of file
#!/usr/bin/python2.4
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Google Inc.
# 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.
......@@ -14,13 +13,15 @@
# 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 base64
import hashlib
import json
import logging
import sys
import time
from anyjson import simplejson
from third_party import six
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
......@@ -38,7 +39,6 @@ class AppIdentityError(Exception):
try:
from OpenSSL import crypto
class OpenSSLVerifier(object):
"""Verifies the signature on a message."""
......@@ -62,6 +62,8 @@ try:
key that this object was constructed with.
"""
try:
if isinstance(message, six.text_type):
message = message.encode('utf-8')
crypto.verify(self._pubkey, signature, message, 'sha256')
return True
except:
......@@ -104,15 +106,17 @@ try:
"""Signs a message.
Args:
message: string, Message to be signed.
message: bytes, Message to be signed.
Returns:
string, The signature of the message for the given key.
"""
if isinstance(message, six.text_type):
message = message.encode('utf-8')
return crypto.sign(self._key, message, 'sha256')
@staticmethod
def from_string(key, password='notasecret'):
def from_string(key, password=b'notasecret'):
"""Construct a Signer instance from a string.
Args:
......@@ -125,21 +129,45 @@ try:
Raises:
OpenSSL.crypto.Error if the key can't be parsed.
"""
if key.startswith('-----BEGIN '):
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
parsed_pem_key = _parse_pem_key(key)
if parsed_pem_key:
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
else:
if isinstance(password, six.text_type):
password = password.encode('utf-8')
pkey = crypto.load_pkcs12(key, password).get_privatekey()
return OpenSSLSigner(pkey)
def pkcs12_key_as_pem(private_key_text, private_key_password):
"""Convert the contents of a PKCS12 key to PEM using OpenSSL.
Args:
private_key_text: String. Private key.
private_key_password: String. Password for PKCS12.
Returns:
String. PEM contents of ``private_key_text``.
"""
decoded_body = base64.b64decode(private_key_text)
if isinstance(private_key_password, six.string_types):
private_key_password = private_key_password.encode('ascii')
pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password)
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
pkcs12.get_privatekey())
except ImportError:
OpenSSLVerifier = None
OpenSSLSigner = None
def pkcs12_key_as_pem(*args, **kwargs):
raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
try:
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from Crypto.Util.asn1 import DerSequence
class PyCryptoVerifier(object):
......@@ -181,14 +209,17 @@ try:
Returns:
Verifier instance.
Raises:
NotImplementedError if is_x509_cert is true.
"""
if is_x509_cert:
raise NotImplementedError(
'X509 certs are not supported by the PyCrypto library. '
'Try using PyOpenSSL if native code is an option.')
if isinstance(key_pem, six.text_type):
key_pem = key_pem.encode('ascii')
pemLines = key_pem.replace(b' ', b'').split()
certDer = _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)
......@@ -214,6 +245,8 @@ try:
Returns:
string, The signature of the message for the given key.
"""
if isinstance(message, six.text_type):
message = message.encode('utf-8')
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
@staticmethod
......@@ -230,11 +263,12 @@ try:
Raises:
NotImplementedError if they key isn't in PEM format.
"""
if key.startswith('-----BEGIN '):
pkey = RSA.importKey(key)
parsed_pem_key = _parse_pem_key(key)
if parsed_pem_key:
pkey = RSA.importKey(parsed_pem_key)
else:
raise NotImplementedError(
'PKCS12 format is not supported by the PyCrpto library. '
'PKCS12 format is not supported by the PyCrypto library. '
'Try converting to a "PEM" '
'(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
'or using PyOpenSSL if native code is an option.')
......@@ -256,19 +290,39 @@ else:
'PyOpenSSL, or PyCrypto 2.6 or later')
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 _urlsafe_b64encode(raw_bytes):
return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
if isinstance(raw_bytes, six.text_type):
raw_bytes = raw_bytes.encode('utf-8')
return base64.urlsafe_b64encode(raw_bytes).decode('ascii').rstrip('=')
def _urlsafe_b64decode(b64string):
# Guard against unicode strings, which base64 can't handle.
if isinstance(b64string, six.text_type):
b64string = b64string.encode('ascii')
padded = b64string + '=' * (4 - len(b64string) % 4)
padded = b64string + b'=' * (4 - len(b64string) % 4)
return base64.urlsafe_b64decode(padded)
def _json_encode(data):
return simplejson.dumps(data, separators = (',', ':'))
return json.dumps(data, separators=(',', ':'))
def make_signed_jwt(signer, payload):
......@@ -318,9 +372,8 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
"""
segments = jwt.split('.')
if (len(segments) != 3):
raise AppIdentityError(
'Wrong number of segments in token: %s' % jwt)
if len(segments) != 3:
raise AppIdentityError('Wrong number of segments in token: %s' % jwt)
signed = '%s.%s' % (segments[0], segments[1])
signature = _urlsafe_b64decode(segments[2])
......@@ -328,15 +381,15 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
# Parse token.
json_body = _urlsafe_b64decode(segments[1])
try:
parsed = simplejson.loads(json_body)
parsed = json.loads(json_body.decode('utf-8'))
except:
raise AppIdentityError('Can\'t parse token: %s' % json_body)
# Check signature.
verified = False
for (keyname, pem) in certs.items():
for pem in certs.values():
verifier = Verifier.from_string(pem, True)
if (verifier.verify(signed, signature)):
if verifier.verify(signed, signature):
verified = True
break
if not verified:
......@@ -349,13 +402,12 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
earliest = iat - CLOCK_SKEW_SECS
# Check expiration timestamp.
now = long(time.time())
now = int(time.time())
exp = parsed.get('exp')
if exp is None:
raise AppIdentityError('No exp field in token: %s' % json_body)
if exp >= now + MAX_TOKEN_LIFETIME_SECS:
raise AppIdentityError(
'exp field too far in future: %s' % json_body)
raise AppIdentityError('exp field too far in future: %s' % json_body)
latest = exp + CLOCK_SKEW_SECS
if now < earliest:
......
# 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 json
import os
from . 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.
"""
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
def _SendRecv():
"""Communicate with the Developer Shell server socket."""
port = int(os.getenv(DEVSHELL_ENV, 0))
if port == 0:
raise NoDevshellServer()
import socket
sock = socket.socket()
sock.connect(('localhost', port))
data = CREDENTIAL_INFO_REQUEST_JSON
msg = '%s\n%s' % (len(data), data)
sock.sendall(msg.encode())
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_request):
self.devshell_response = _SendRecv()
self.access_token = self.devshell_response.access_token
@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.')
# Copyright (C) 2010 Google Inc.
# 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.
......@@ -116,14 +116,21 @@ class Storage(BaseStorage):
credential.set_store(self)
return credential
def locked_put(self, credentials):
def locked_put(self, credentials, overwrite=False):
"""Write a Credentials to the datastore.
Args:
credentials: Credentials, the credentials to store.
overwrite: Boolean, indicates whether you would like these credentials to
overwrite any existing stored credentials.
"""
args = {self.key_name: self.key_value}
if overwrite:
entity, unused_is_new = self.model_class.objects.get_or_create(**args)
else:
entity = self.model_class(**args)
setattr(entity, self.property_name, credentials)
entity.save()
......
# Copyright (C) 2010 Google Inc.
# 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.
......@@ -21,12 +21,10 @@ credentials.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import os
import stat
import threading
from anyjson import simplejson
from client import Storage as BaseStorage
from client import Credentials
from client import Storage as BaseStorage
class CredentialsFileSymbolicLinkError(Exception):
......@@ -92,7 +90,7 @@ class Storage(BaseStorage):
simple version of "touch" to ensure the file has been created.
"""
if not os.path.exists(self._filename):
old_umask = os.umask(0177)
old_umask = os.umask(0o177)
try:
open(self._filename, 'a+b').close()
finally:
......@@ -110,7 +108,7 @@ class Storage(BaseStorage):
self._create_file_if_needed()
self._validate_file()
f = open(self._filename, 'wb')
f = open(self._filename, 'w')
f.write(credentials.to_json())
f.close()
......
# Copyright (C) 2012 Google Inc.
# 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.
......@@ -19,14 +19,13 @@ Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import httplib2
import json
import logging
import uritemplate
from third_party.six.moves import urllib
from oauth2client import util
from oauth2client.anyjson import simplejson
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import AssertionCredentials
from third_party.oauth2client import util
from third_party.oauth2client.client import AccessTokenRefreshError
from third_party.oauth2client.client import AssertionCredentials
logger = logging.getLogger(__name__)
......@@ -57,13 +56,14 @@ class AppAssertionCredentials(AssertionCredentials):
requested.
"""
self.scope = util.scopes_to_string(scope)
self.kwargs = kwargs
# 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 = simplejson.loads(json)
def from_json(cls, json_data):
data = json.loads(json_data)
return AppAssertionCredentials(data['scope'])
def _refresh(self, http_request):
......@@ -78,13 +78,28 @@ class AppAssertionCredentials(AssertionCredentials):
Raises:
AccessTokenRefreshError: When the refresh fails.
"""
uri = uritemplate.expand(META, {'scope': self.scope})
query = '?scope=%s' % urllib.parse.quote(self.scope, '')
uri = META.replace('{?scope}', query)
response, content = http_request(uri)
if response.status == 200:
try:
d = simplejson.loads(content)
except StandardError, e:
d = json.loads(content)
except Exception as e:
raise AccessTokenRefreshError(str(e))
self.access_token = d['accessToken']
else:
if response.status == 404:
content += (' This can occur if a VM was created'
' with no service account or scopes.')
raise AccessTokenRefreshError(content)
@property
def serialization_data(self):
raise NotImplementedError(
'Cannot serialize credentials for GCE service accounts.')
def create_scoped_required(self):
return not self.scope
def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self.kwargs)
\ No newline at end of file
# Copyright (C) 2012 Google Inc.
# 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.
......@@ -19,11 +19,12 @@ A Storage for Credentials that uses the keyring module.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import keyring
import threading
from client import Storage as BaseStorage
import keyring
from client import Credentials
from client import Storage as BaseStorage
class Storage(BaseStorage):
......
# Copyright 2011 Google Inc.
# 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.
......@@ -17,17 +17,21 @@
This module first tries to use fcntl locking to ensure serialized access
to a file, then falls back on a lock file if that is unavialable.
Usage:
Usage::
f = LockedFile('filename', 'r+b', 'rb')
f.open_and_lock()
if f.is_locked():
print 'Acquired filename with r+b mode'
print('Acquired filename with r+b mode')
f.file_handle().write('locked data')
else:
print 'Aquired filename with rb mode'
print('Acquired filename with rb mode')
f.unlock_and_close()
"""
from __future__ import print_function
__author__ = 'cache@google.com (David T McWherter)'
import errno
......@@ -70,6 +74,7 @@ class _Opener(object):
self._mode = mode
self._fallback_mode = fallback_mode
self._fh = None
self._lock_fd = None
def is_locked(self):
"""Was the file locked."""
......@@ -122,7 +127,7 @@ class _PosixOpener(_Opener):
validate_file(self._filename)
try:
self._fh = open(self._filename, self._mode)
except IOError, e:
except IOError as e:
# If we can't access with _mode, try _fallback_mode and don't lock.
if e.errno == errno.EACCES:
self._fh = open(self._filename, self._fallback_mode)
......@@ -137,12 +142,12 @@ class _PosixOpener(_Opener):
self._locked = True
break
except OSError, e:
except OSError as e:
if e.errno != errno.EEXIST:
raise
if (time.time() - start_time) >= timeout:
logger.warn('Could not acquire lock %s in %s seconds' % (
lock_filename, timeout))
logger.warn('Could not acquire lock %s in %s seconds',
lock_filename, timeout)
# Close the file and open in fallback_mode.
if self._fh:
self._fh.close()
......@@ -192,9 +197,9 @@ try:
validate_file(self._filename)
try:
self._fh = open(self._filename, self._mode)
except IOError, e:
except IOError as e:
# If we can't access with _mode, try _fallback_mode and don't lock.
if e.errno == errno.EACCES:
if e.errno in (errno.EPERM, errno.EACCES):
self._fh = open(self._filename, self._fallback_mode)
return
......@@ -204,16 +209,16 @@ try:
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
self._locked = True
return
except IOError, e:
except IOError as e:
# If not retrying, then just pass on the error.
if timeout == 0:
raise e
raise
if e.errno != errno.EACCES:
raise e
raise
# We could not acquire the lock. Try again.
if (time.time() - start_time) >= timeout:
logger.warn('Could not lock %s in %s seconds' % (
self._filename, timeout))
logger.warn('Could not lock %s in %s seconds',
self._filename, timeout)
if self._fh:
self._fh.close()
self._fh = open(self._filename, self._fallback_mode)
......@@ -267,7 +272,7 @@ try:
validate_file(self._filename)
try:
self._fh = open(self._filename, self._mode)
except IOError, e:
except IOError as e:
# If we can't access with _mode, try _fallback_mode and don't lock.
if e.errno == errno.EACCES:
self._fh = open(self._filename, self._fallback_mode)
......@@ -284,9 +289,9 @@ try:
pywintypes.OVERLAPPED())
self._locked = True
return
except pywintypes.error, e:
except pywintypes.error as e:
if timeout == 0:
raise e
raise
# If the error is not that the file is already in use, raise.
if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
......@@ -308,7 +313,7 @@ try:
try:
hfile = win32file._get_osfhandle(self._fh.fileno())
win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED())
except pywintypes.error, e:
except pywintypes.error as e:
if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
raise
self._locked = False
......
# Copyright 2011 Google Inc.
# 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.
......@@ -19,12 +19,14 @@ credentials can be stored in one file. That file supports locking
both in a single process and across processes.
The credential themselves are keyed off of:
* client_id
* user_agent
* scope
The format of the stored data is like so:
{
The format of the stored data is like so::
{
'file_version': 1,
'data': [
{
......@@ -38,20 +40,20 @@ The format of the stored data is like so:
}
}
]
}
}
"""
__author__ = 'jbeda@google.com (Joe Beda)'
import base64
import errno
import json
import logging
import os
import threading
from anyjson import simplejson
from .client import Storage as BaseStorage
from .client import Credentials
from .client import Storage as BaseStorage
from . import util
from locked_file import LockedFile
......@@ -64,12 +66,10 @@ _multistores_lock = threading.Lock()
class Error(Exception):
"""Base error for this module."""
pass
class NewerCredentialStoreError(Error):
"""The credential store is a newer version that supported."""
pass
"""The credential store is a newer version than supported."""
@util.positional(4)
......@@ -193,7 +193,7 @@ class _MultiStore(object):
This will create the file if necessary.
"""
self._file = LockedFile(filename, 'r+b', 'rb')
self._file = LockedFile(filename, 'r+', 'r')
self._thread_lock = threading.Lock()
self._read_only = False
self._warn_on_readonly = warn_on_readonly
......@@ -271,7 +271,7 @@ class _MultiStore(object):
simple version of "touch" to ensure the file has been created.
"""
if not os.path.exists(self._file.filename()):
old_umask = os.umask(0177)
old_umask = os.umask(0o177)
try:
open(self._file.filename(), 'a+b').close()
finally:
......@@ -280,13 +280,23 @@ class _MultiStore(object):
def _lock(self):
"""Lock the entire multistore."""
self._thread_lock.acquire()
try:
self._file.open_and_lock()
except IOError as e:
if e.errno == errno.ENOSYS:
logger.warn('File system does not support locking the credentials '
'file.')
elif e.errno == errno.ENOLCK:
logger.warn('File system is out of resources for writing the '
'credentials file (is your disk full?).')
else:
raise
if not self._file.is_locked():
self._read_only = True
if self._warn_on_readonly:
logger.warn('The credentials file (%s) is not writable. Opening in '
'read-only mode. Any refreshed credentials will only be '
'valid for this run.' % self._file.filename())
'valid for this run.', self._file.filename())
if os.path.getsize(self._file.filename()) == 0:
logger.debug('Initializing empty multistore file')
# The multistore is empty so write out an empty file.
......@@ -315,7 +325,7 @@ class _MultiStore(object):
"""
assert self._thread_lock.locked()
self._file.file_handle().seek(0)
return simplejson.load(self._file.file_handle())
return json.load(self._file.file_handle())
def _locked_json_write(self, data):
"""Write a JSON serializable data structure to the multistore.
......@@ -329,7 +339,7 @@ class _MultiStore(object):
if self._read_only:
return
self._file.file_handle().seek(0)
simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2)
json.dump(data, self._file.file_handle(), sort_keys=True, indent=2, separators=(',', ': '))
self._file.file_handle().truncate()
def _refresh_data_cache(self):
......@@ -387,7 +397,7 @@ class _MultiStore(object):
raw_key = cred_entry['key']
key = util.dict_to_tuple_key(raw_key)
credential = None
credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential']))
credential = Credentials.new_from_json(json.dumps(cred_entry['credential']))
return (key, credential)
def _write(self):
......@@ -400,7 +410,7 @@ class _MultiStore(object):
raw_data['data'] = raw_creds
for (cred_key, cred) in self._data.items():
raw_key = dict(cred_key)
raw_cred = simplejson.loads(cred.to_json())
raw_cred = json.loads(cred.to_json())
raw_creds.append({'key': raw_key, 'credential': raw_cred})
self._locked_json_write(raw_data)
......
# Copyright (C) 2013 Google Inc.
# 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.
......@@ -15,6 +15,7 @@
"""This module holds the old run() function which is deprecated, the
tools.run_flow() function should be used in its place."""
from __future__ import print_function
import logging
import socket
......@@ -22,9 +23,9 @@ import sys
import webbrowser
import gflags
from oauth2client import client
from oauth2client import util
from third_party.six.moves import input
from third_party.oauth2client import client
from third_party.oauth2client import util
from tools import ClientRedirectHandler
from tools import ClientRedirectServer
......@@ -48,39 +49,38 @@ gflags.DEFINE_multi_int('auth_host_port', [8080, 8090],
def run(flow, storage, 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.
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: Host name to use when running a local web server
to handle redirects during OAuth authorization.
(default: 'localhost')
``--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: Port to use when running a local web server to handle
redirects during OAuth authorization.;
repeat this option to specify a list of values
(default: '[8080, 8090]')
(an integer)
``--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: Run a local web server to handle redirects
during OAuth authorization.
(default: 'true')
``--[no]auth_local_webserver`` (boolean, default: ``True``)
Run a local web server to handle redirects during OAuth authorization.
Since it uses flags make sure to initialize the gflags module before
calling run().
Since it uses flags make sure to initialize the ``gflags`` module before
calling ``run()``.
Args:
flow: Flow, an OAuth 2.0 Flow to step through.
storage: Storage, a Storage to store the credential in.
http: An instance of httplib2.Http.request
or something that acts like it.
storage: Storage, a ``Storage`` to store the credential in.
http: An instance of ``httplib2.Http.request`` or something that acts
like it.
Returns:
Credentials, the obtained credential.
......@@ -96,20 +96,20 @@ def run(flow, storage, http=None):
try:
httpd = ClientRedirectServer((FLAGS.auth_host_name, port),
ClientRedirectHandler)
except socket.error, e:
except socket.error as e:
pass
else:
success = True
break
FLAGS.auth_local_webserver = success
if not success:
print 'Failed to start a local webserver listening on either port 8080'
print 'or port 9090. Please check your firewall settings and locally'
print 'running programs that may be blocking or using those ports.'
print
print 'Falling back to --noauth_local_webserver and continuing with',
print 'authorization.'
print
print('Failed to start a local webserver listening on either port 8080')
print('or port 9090. Please check your firewall settings and locally')
print('running programs that may be blocking or using those ports.')
print()
print('Falling back to --noauth_local_webserver and continuing with')
print('authorization.')
print()
if FLAGS.auth_local_webserver:
oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number)
......@@ -120,20 +120,20 @@ def run(flow, storage, http=None):
if FLAGS.auth_local_webserver:
webbrowser.open(authorize_url, new=1, autoraise=True)
print 'Your browser has been opened to visit:'
print
print ' ' + authorize_url
print
print 'If your browser is on a different machine then exit and re-run'
print 'this application with the command-line parameter '
print
print ' --noauth_local_webserver'
print
print('Your browser has been opened to visit:')
print()
print(' ' + authorize_url)
print()
print('If your browser is on a different machine then exit and re-run')
print('this application with the command-line parameter ')
print()
print(' --noauth_local_webserver')
print()
else:
print 'Go to the following link in your browser:'
print
print ' ' + authorize_url
print
print('Go to the following link in your browser:')
print()
print(' ' + authorize_url)
print()
code = None
if FLAGS.auth_local_webserver:
......@@ -143,18 +143,18 @@ def run(flow, storage, http=None):
if 'code' in httpd.query_params:
code = httpd.query_params['code']
else:
print 'Failed to find "code" in the query parameters of the redirect.'
print('Failed to find "code" in the query parameters of the redirect.')
sys.exit('Try running with --noauth_local_webserver.')
else:
code = raw_input('Enter verification code: ').strip()
code = input('Enter verification code: ').strip()
try:
credential = flow.step2_exchange(code, http=http)
except client.FlowExchangeError, e:
except client.FlowExchangeError as e:
sys.exit('Authentication has failed: %s' % e)
storage.put(credential)
credential.set_store(storage)
print 'Authentication successful.'
print('Authentication successful.')
return credential
\ No newline at end of file
# 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 service account credentials class.
This credentials class is implemented on top of rsa library.
"""
import base64
import json
import time
from pyasn1.codec.ber import decoder
from pyasn1_modules.rfc5208 import PrivateKeyInfo
import rsa
from . import GOOGLE_REVOKE_URI
from . import GOOGLE_TOKEN_URI
from . import util
from client import AssertionCredentials
from third_party import six
class _ServiceAccountCredentials(AssertionCredentials):
"""Class representing a service account (signed JWT) credential."""
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
def __init__(self, service_account_id, service_account_email, private_key_id,
private_key_pkcs8_text, scopes, user_agent=None,
token_uri=GOOGLE_TOKEN_URI, revoke_uri=GOOGLE_REVOKE_URI,
**kwargs):
super(_ServiceAccountCredentials, self).__init__(
None, user_agent=user_agent, token_uri=token_uri, revoke_uri=revoke_uri)
self._service_account_id = service_account_id
self._service_account_email = service_account_email
self._private_key_id = private_key_id
self._private_key = _get_private_key(private_key_pkcs8_text)
self._private_key_pkcs8_text = private_key_pkcs8_text
self._scopes = util.scopes_to_string(scopes)
self._user_agent = user_agent
self._token_uri = token_uri
self._revoke_uri = revoke_uri
self._kwargs = kwargs
def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""
header = {
'alg': 'RS256',
'typ': 'JWT',
'kid': self._private_key_id
}
now = int(time.time())
payload = {
'aud': self._token_uri,
'scope': self._scopes,
'iat': now,
'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS,
'iss': self._service_account_email
}
payload.update(self._kwargs)
assertion_input = (_urlsafe_b64encode(header) + b'.' +
_urlsafe_b64encode(payload))
# Sign the assertion.
rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key, 'SHA-256')
signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=')
return assertion_input + b'.' + signature
def sign_blob(self, blob):
# Ensure that it is bytes
try:
blob = blob.encode('utf-8')
except AttributeError:
pass
return (self._private_key_id,
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
@property
def service_account_email(self):
return self._service_account_email
@property
def serialization_data(self):
return {
'type': 'service_account',
'client_id': self._service_account_id,
'client_email': self._service_account_email,
'private_key_id': self._private_key_id,
'private_key': self._private_key_pkcs8_text
}
def create_scoped_required(self):
return not self._scopes
def create_scoped(self, scopes):
return _ServiceAccountCredentials(self._service_account_id,
self._service_account_email,
self._private_key_id,
self._private_key_pkcs8_text,
scopes,
user_agent=self._user_agent,
token_uri=self._token_uri,
revoke_uri=self._revoke_uri,
**self._kwargs)
def _urlsafe_b64encode(data):
return base64.urlsafe_b64encode(
json.dumps(data, separators=(',', ':')).encode('UTF-8')).rstrip(b'=')
def _get_private_key(private_key_pkcs8_text):
"""Get an RSA private key object from a pkcs8 representation."""
if not isinstance(private_key_pkcs8_text, six.binary_type):
private_key_pkcs8_text = private_key_pkcs8_text.encode('ascii')
der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY')
asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo())
return rsa.PrivateKey.load_pkcs1(
asn1_private_key.getComponentByName('privateKey').asOctets(),
format='DER')
\ No newline at end of file
# Copyright (C) 2013 Google Inc.
# 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.
......@@ -19,27 +19,22 @@ generated credentials in a common file that is used by other example apps in
the same directory.
"""
from __future__ import print_function
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing']
import BaseHTTPServer
import argparse
import httplib2
import logging
import os
import socket
import sys
import webbrowser
from oauth2client import client
from oauth2client import file
from oauth2client import util
from third_party.six.moves import BaseHTTPServer
from third_party.six.moves import urllib
from third_party.six.moves import input
from . import client
from . import util
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
......@@ -52,20 +47,27 @@ with information from the APIs Console <https://code.google.com/apis/console>.
"""
# run_parser 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 = argparse.ArgumentParser(add_help=False)
argparser.add_argument('--auth_host_name', default='localhost',
def _CreateArgumentParser():
try:
import argparse
except ImportError:
return None
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--auth_host_name', default='localhost',
help='Hostname when running a local web server.')
argparser.add_argument('--noauth_local_webserver', action='store_true',
parser.add_argument('--noauth_local_webserver', action='store_true',
default=False, help='Do not run a local web server.')
argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
nargs='*', help='Port web server should listen on.')
argparser.add_argument('--logging_level', default='ERROR',
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR',
'CRITICAL'],
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):
......@@ -84,72 +86,75 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
into the servers query_params and then stops serving.
"""
def do_GET(s):
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.
"""
s.send_response(200)
s.send_header("Content-type", "text/html")
s.end_headers()
query = s.path.split('?', 1)[-1]
query = dict(parse_qsl(query))
s.server.query_params = query
s.wfile.write("<html><head><title>Authentication Status</title></head>")
s.wfile.write("<body><p>The authentication flow has completed.</p>")
s.wfile.write("</body></html>")
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
query = self.path.split('?', 1)[-1]
query = dict(urllib.parse.parse_qsl(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 command line program."""
pass
@util.positional(3)
def run_flow(flow, storage, flags, 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.
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: Host name to use when running a local web server
to handle redirects during OAuth authorization.
(default: 'localhost')
``--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.
--auth_host_port: Port to use when running a local web server to handle
redirects during OAuth authorization.;
repeat this option to specify a list of values
(default: '[8080, 8090]')
(an integer)
--[no]auth_local_webserver: Run a local web server to handle redirects
during OAuth authorization.
(default: 'true')
The tools module defines an ArgumentParser the already contains the flag
definitions that run() requires. You can pass that ArgumentParser to your
ArgumentParser constructor:
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.run_parser])
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.ArgumentParser, the command-line flags.
http: An instance of httplib2.Http.request
or something that acts like it.
storage: Storage, a ``Storage`` to store the credential in.
flags: ``argparse.Namespace``, The command-line flags. This is the
object returned from calling ``parse_args()`` on
``argparse.ArgumentParser`` as described above.
http: An instance of ``httplib2.Http.request`` or something that
acts like it.
Returns:
Credentials, the obtained credential.
......@@ -163,20 +168,20 @@ def run_flow(flow, storage, flags, http=None):
try:
httpd = ClientRedirectServer((flags.auth_host_name, port),
ClientRedirectHandler)
except socket.error, e:
except socket.error:
pass
else:
success = True
break
flags.noauth_local_webserver = not success
if not success:
print 'Failed to start a local webserver listening on either port 8080'
print 'or port 9090. Please check your firewall settings and locally'
print 'running programs that may be blocking or using those ports.'
print
print 'Falling back to --noauth_local_webserver and continuing with',
print 'authorization.'
print
print('Failed to start a local webserver listening on either port 8080')
print('or port 9090. Please check your firewall settings and locally')
print('running programs that may be blocking or using those ports.')
print()
print('Falling back to --noauth_local_webserver and continuing with')
print('authorization.')
print()
if not flags.noauth_local_webserver:
oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
......@@ -186,21 +191,22 @@ def run_flow(flow, storage, flags, http=None):
authorize_url = flow.step1_get_authorize_url()
if not flags.noauth_local_webserver:
import webbrowser
webbrowser.open(authorize_url, new=1, autoraise=True)
print 'Your browser has been opened to visit:'
print
print ' ' + authorize_url
print
print 'If your browser is on a different machine then exit and re-run this'
print 'application with the command-line parameter '
print
print ' --noauth_local_webserver'
print
print('Your browser has been opened to visit:')
print()
print(' ' + authorize_url)
print()
print('If your browser is on a different machine then exit and re-run this')
print('application with the command-line parameter ')
print()
print(' --noauth_local_webserver')
print()
else:
print 'Go to the following link in your browser:'
print
print ' ' + authorize_url
print
print('Go to the following link in your browser:')
print()
print(' ' + authorize_url)
print()
code = None
if not flags.noauth_local_webserver:
......@@ -210,19 +216,19 @@ def run_flow(flow, storage, flags, http=None):
if 'code' in httpd.query_params:
code = httpd.query_params['code']
else:
print 'Failed to find "code" in the query parameters of the redirect.'
print('Failed to find "code" in the query parameters of the redirect.')
sys.exit('Try running with --noauth_local_webserver.')
else:
code = raw_input('Enter verification code: ').strip()
code = input('Enter verification code: ').strip()
try:
credential = flow.step2_exchange(code, http=http)
except client.FlowExchangeError, e:
except client.FlowExchangeError as e:
sys.exit('Authentication has failed: %s' % e)
storage.put(credential)
credential.set_store(storage)
print 'Authentication successful.'
print('Authentication successful.')
return credential
......@@ -233,8 +239,8 @@ def message_if_missing(filename):
return _CLIENT_SECRETS_MESSAGE % filename
try:
from old_run import run
from old_run import FLAGS
from oauth2client.old_run import run
from oauth2client.old_run import FLAGS
except ImportError:
def run(*args, **kwargs):
raise NotImplementedError(
......
#!/usr/bin/env python
#
# Copyright 2010 Google Inc.
# 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.
......@@ -17,9 +17,11 @@
"""Common utility library."""
__author__ = ['rafek@google.com (Rafe Kaplan)',
__author__ = [
'rafek@google.com (Rafe Kaplan)',
'guido@google.com (Guido van Rossum)',
]
__all__ = [
'positional',
'POSITIONAL_WARNING',
......@@ -27,16 +29,15 @@ __all__ = [
'POSITIONAL_IGNORE',
]
import functools
import inspect
import logging
import sys
import types
import urllib
import urlparse
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
from third_party import six
from third_party.six.moves import urllib
logger = logging.getLogger(__name__)
......@@ -51,39 +52,41 @@ positional_parameters_enforcement = POSITIONAL_WARNING
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 key-word only
parameters. For example, in Python 3 it is possible to write:
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:
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:
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:
keyword argument::
@positional(0)
def fn(required_kw):
...
This must be called with the keyword parameter:
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':
``self`` and ``cls``::
class MyClass(object):
......@@ -97,10 +100,10 @@ def positional(max_positional_args):
...
The positional decorator behavior is controlled by
util.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.
``util.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
......@@ -114,8 +117,10 @@ def positional(max_positional_args):
TypeError if a key-word only argument is provided as a positional
parameter, but only if util.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 = ''
......@@ -132,7 +137,7 @@ def positional(max_positional_args):
return wrapped(*args, **kwargs)
return positional_wrapper
if isinstance(max_positional_args, (int, long)):
if isinstance(max_positional_args, six.integer_types):
return positional_decorator
else:
args, _, _, defaults = inspect.getargspec(max_positional_args)
......@@ -152,7 +157,7 @@ def scopes_to_string(scopes):
Returns:
The scopes formatted as a single string.
"""
if isinstance(scopes, types.StringTypes):
if isinstance(scopes, six.string_types):
return scopes
else:
return ' '.join(scopes)
......@@ -189,8 +194,8 @@ def _add_query_parameter(url, name, value):
if value is None:
return url
else:
parsed = list(urlparse.urlparse(url))
q = dict(parse_qsl(parsed[4]))
parsed = list(urllib.parse.urlparse(url))
q = dict(urllib.parse.parse_qsl(parsed[4]))
q[name] = value
parsed[4] = urllib.urlencode(q)
return urlparse.urlunparse(parsed)
parsed[4] = urllib.parse.urlencode(q)
return urllib.parse.urlunparse(parsed)
\ No newline at end of file
#!/usr/bin/python2.5
#
# Copyright 2010 the Melange authors.
# 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.
......@@ -24,18 +23,29 @@ __authors__ = [
import base64
import hmac
import os # for urandom
import time
import six
from oauth2client import util
# Delimiter character
DELIMITER = ':'
DELIMITER = b':'
# 1 hour in seconds
DEFAULT_TIMEOUT_SECS = 1*60*60
def _force_bytes(s):
if isinstance(s, bytes):
return s
s = str(s)
if isinstance(s, six.text_type):
return s.encode('utf-8')
return s
@util.positional(2)
def generate_token(key, user_id, action_id="", when=None):
"""Generates a URL-safe token for the given user, action, time tuple.
......@@ -51,18 +61,16 @@ def generate_token(key, user_id, action_id="", when=None):
Returns:
A string XSRF protection token.
"""
when = when or int(time.time())
digester = hmac.new(key)
digester.update(str(user_id))
when = _force_bytes(when or int(time.time()))
digester = hmac.new(_force_bytes(key))
digester.update(_force_bytes(user_id))
digester.update(DELIMITER)
digester.update(action_id)
digester.update(_force_bytes(action_id))
digester.update(DELIMITER)
digester.update(str(when))
digester.update(when)
digest = digester.digest()
token = base64.urlsafe_b64encode('%s%s%d' % (digest,
DELIMITER,
when))
token = base64.urlsafe_b64encode(digest + DELIMITER + when)
return token
......@@ -87,8 +95,8 @@ def validate_token(key, token, user_id, action_id="", current_time=None):
if not token:
return False
try:
decoded = base64.urlsafe_b64decode(str(token))
token_time = long(decoded.split(DELIMITER)[-1])
decoded = base64.urlsafe_b64decode(token)
token_time = int(decoded.split(DELIMITER)[-1])
except (TypeError, ValueError):
return False
if current_time is None:
......@@ -105,9 +113,6 @@ def validate_token(key, token, user_id, action_id="", current_time=None):
# Perform constant time comparison to avoid timing attacks
different = 0
for x, y in zip(token, expected_token):
different |= ord(x) ^ ord(y)
if different:
return False
return True
for x, y in zip(bytearray(token), bytearray(expected_token)):
different |= x ^ y
return not different
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment