Adding a .revoke() to Credentials. Closes issue 98.

Reviewed in https://codereview.appspot.com/7033052/
This commit is contained in:
dhermes@google.com
2013-02-06 09:19:01 -08:00
parent 7014343384
commit a9eb0bb68f
10 changed files with 387 additions and 156 deletions

View File

@@ -1 +1,5 @@
__version__ = "1.0"
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth'
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'

View File

@@ -35,6 +35,9 @@ from google.appengine.ext import ndb
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import login_required
from google.appengine.ext.webapp.util import run_wsgi_app
from oauth2client import GOOGLE_AUTH_URI
from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import clientsecrets
from oauth2client import util
from oauth2client import xsrfutil
@@ -553,8 +556,9 @@ class OAuth2Decorator(object):
@util.positional(4)
def __init__(self, client_id, client_secret, scope,
auth_uri='https://accounts.google.com/o/oauth2/auth',
token_uri='https://accounts.google.com/o/oauth2/token',
auth_uri=GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
user_agent=None,
message=None,
callback_path='/oauth2callback',
@@ -571,6 +575,8 @@ class OAuth2Decorator(object):
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
user_agent: string, User agent of your application, default to None.
message: Message to display if there are problems with the OAuth 2.0
configuration. The message may contain HTML and will be presented on the
@@ -588,6 +594,7 @@ class OAuth2Decorator(object):
self._scope = util.scopes_to_string(scope)
self._auth_uri = auth_uri
self._token_uri = token_uri
self._revoke_uri = revoke_uri
self._user_agent = user_agent
self._kwargs = kwargs
self._message = message
@@ -655,8 +662,9 @@ class OAuth2Decorator(object):
self._scope, redirect_uri=redirect_uri,
user_agent=self._user_agent,
auth_uri=self._auth_uri,
token_uri=self._token_uri, **self._kwargs)
token_uri=self._token_uri,
revoke_uri=self._revoke_uri,
**self._kwargs)
def oauth_aware(self, method):
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
@@ -827,17 +835,21 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
raise InvalidClientSecretsError(
'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
constructor_kwargs = {
'auth_uri': client_info['auth_uri'],
'token_uri': client_info['token_uri'],
'message': message,
}
revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None:
constructor_kwargs['revoke_uri'] = revoke_uri
super(OAuth2DecoratorFromClientSecrets, self).__init__(
client_info['client_id'],
client_info['client_secret'],
scope,
auth_uri=client_info['auth_uri'],
token_uri=client_info['token_uri'],
message=message)
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)
if message is not None:
self._message = message
else:
self._message = "Please configure your application for OAuth 2.0"
self._message = 'Please configure your application for OAuth 2.0.'
@util.positional(2)

View File

@@ -31,6 +31,9 @@ import time
import urllib
import urlparse
from oauth2client import GOOGLE_AUTH_URI
from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import util
from oauth2client.anyjson import simplejson
@@ -63,36 +66,34 @@ REFRESH_STATUS_CODES = [401]
class Error(Exception):
"""Base error for this module."""
pass
class FlowExchangeError(Error):
"""Error trying to exchange an authorization grant for an access token."""
pass
class AccessTokenRefreshError(Error):
"""Error trying to refresh an expired access token."""
pass
class TokenRevokeError(Error):
"""Error trying to revoke a token."""
class UnknownClientSecretsFlowError(Error):
"""The client secrets file called for an unknown type of OAuth 2.0 flow. """
pass
class AccessTokenCredentialsError(Error):
"""Having only the access_token means no refresh is possible."""
pass
class VerifyJwtTokenError(Error):
"""Could on retrieve certificates for validation."""
pass
class NonAsciiHeaderError(Error):
"""Header names and values must be ASCII strings."""
pass
def _abstract():
@@ -128,11 +129,15 @@ class Credentials(object):
NON_SERIALIZED_MEMBERS = ['store']
def authorize(self, http):
"""Take an httplib2.Http instance (or equivalent) and
authorizes it for the set of credentials, usually by
replacing http.request() with a method that adds in
the appropriate headers and then delegates to the original
Http.request() method.
"""Take an httplib2.Http instance (or equivalent) and authorizes it.
Authorizes it for the set of credentials, usually by replacing
http.request() with a method that adds in the appropriate headers and then
delegates to the original Http.request() method.
Args:
http: httplib2.Http, an http object to be used to make the refresh
request.
"""
_abstract()
@@ -145,6 +150,15 @@ class Credentials(object):
"""
_abstract()
def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void.
Args:
http: httplib2.Http, an http object to be used to make the revoke
request.
"""
_abstract()
def apply(self, headers):
"""Add the authorization to the headers.
@@ -154,7 +168,7 @@ class Credentials(object):
_abstract()
def _to_json(self, strip):
"""Utility function for creating a JSON representation of an instance of Credentials.
"""Utility function that creates JSON repr. of a Credentials object.
Args:
strip: array, An array of names of members to not include in the JSON.
@@ -347,6 +361,23 @@ def clean_headers(headers):
return clean
def _update_query_params(uri, params):
"""Updates a URI with new query parameters.
Args:
uri: string, A valid URI, with potential existing query parameters.
params: dict, A dictionary of query parameters.
Returns:
The same URI but with the new query parameters added.
"""
parts = list(urlparse.urlparse(uri))
query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
query_params.update(params)
parts[4] = urllib.urlencode(query_params)
return urlparse.urlunparse(parts)
class OAuth2Credentials(Credentials):
"""Credentials object for OAuth 2.0.
@@ -358,7 +389,8 @@ class OAuth2Credentials(Credentials):
@util.positional(8)
def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, id_token=None):
token_expiry, token_uri, user_agent, revoke_uri=None,
id_token=None):
"""Create an instance of OAuth2Credentials.
This constructor is not usually called by the user, instead
@@ -372,6 +404,8 @@ class OAuth2Credentials(Credentials):
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 None; a token
can't be revoked if this is None.
id_token: object, The identity of the resource owner.
Notes:
@@ -388,6 +422,7 @@ class OAuth2Credentials(Credentials):
self.token_expiry = token_expiry
self.token_uri = token_uri
self.user_agent = user_agent
self.revoke_uri = revoke_uri
self.id_token = id_token
# True if the credentials have been revoked or expired and can't be
@@ -473,6 +508,15 @@ class OAuth2Credentials(Credentials):
"""
self._refresh(http.request)
def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void.
Args:
http: httplib2.Http, an http object to be used to make the revoke
request.
"""
self._revoke(http.request)
def apply(self, headers):
"""Add the authorization to the headers.
@@ -511,6 +555,7 @@ class OAuth2Credentials(Credentials):
data['token_expiry'],
data['token_uri'],
data['user_agent'],
revoke_uri=data.get('revoke_uri', None),
id_token=data.get('id_token', None))
retval.invalid = data['invalid']
return retval
@@ -655,6 +700,46 @@ class OAuth2Credentials(Credentials):
pass
raise AccessTokenRefreshError(error_msg)
def _revoke(self, http_request):
"""Revokes the refresh_token and deletes the store if available.
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)
def _do_revoke(self, http_request, token):
"""Revokes the credentials and deletes the store if available.
Args:
http_request: callable, a callable that matches the method signature of
httplib2.Http.request, used to make the refresh request.
token: A string used as the token to be revoked. Can be either an
access_token or refresh_token.
Raises:
TokenRevokeError: If the revoke request does not return with a 200 OK.
"""
logger.info('Revoking token')
query_params = {'token': token}
token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
resp, content = http_request(token_revoke_uri)
if resp.status == 200:
self.invalid = True
else:
error_msg = 'Invalid response %s.' % resp.status
try:
d = simplejson.loads(content)
if 'error' in d:
error_msg = d['error']
except StandardError:
pass
raise TokenRevokeError(error_msg)
if self.store:
self.store.delete()
class AccessTokenCredentials(OAuth2Credentials):
"""Credentials object for OAuth 2.0.
@@ -681,7 +766,7 @@ class AccessTokenCredentials(OAuth2Credentials):
revoked.
"""
def __init__(self, access_token, user_agent):
def __init__(self, access_token, user_agent, revoke_uri=None):
"""Create an instance of OAuth2Credentials
This is one of the few types if Credentials that you should contrust,
@@ -690,10 +775,8 @@ class AccessTokenCredentials(OAuth2Credentials):
Args:
access_token: string, access token.
user_agent: string, The HTTP User-Agent to provide for this application.
Notes:
store: callable, a callable that when passed a Credential
will store the credential back to where it came from.
revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
can't be revoked if this is None.
"""
super(AccessTokenCredentials, self).__init__(
access_token,
@@ -702,7 +785,8 @@ class AccessTokenCredentials(OAuth2Credentials):
None,
None,
None,
user_agent)
user_agent,
revoke_uri=revoke_uri)
@classmethod
@@ -715,7 +799,16 @@ class AccessTokenCredentials(OAuth2Credentials):
def _refresh(self, http_request):
raise AccessTokenCredentialsError(
"The access_token is expired or invalid and can't be refreshed.")
'The access_token is expired or invalid and can\'t be refreshed.')
def _revoke(self, http_request):
"""Revokes the access_token and deletes the store if available.
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.access_token)
class AssertionCredentials(OAuth2Credentials):
@@ -731,7 +824,8 @@ class AssertionCredentials(OAuth2Credentials):
@util.positional(2)
def __init__(self, assertion_type, user_agent=None,
token_uri='https://accounts.google.com/o/oauth2/token',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
**unused_kwargs):
"""Constructor for AssertionFlowCredentials.
@@ -741,6 +835,7 @@ class AssertionCredentials(OAuth2Credentials):
user_agent: string, The HTTP User-Agent to provide for this application.
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
revoke_uri: string, URI for revoke endpoint.
"""
super(AssertionCredentials, self).__init__(
None,
@@ -749,7 +844,8 @@ class AssertionCredentials(OAuth2Credentials):
None,
None,
token_uri,
user_agent)
user_agent,
revoke_uri=revoke_uri)
self.assertion_type = assertion_type
def _generate_refresh_request_body(self):
@@ -769,6 +865,16 @@ class AssertionCredentials(OAuth2Credentials):
"""
_abstract()
def _revoke(self, http_request):
"""Revokes the access_token and deletes the store if available.
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.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
@@ -794,7 +900,8 @@ if HAS_CRYPTO:
scope,
private_key_password='notasecret',
user_agent=None,
token_uri='https://accounts.google.com/o/oauth2/token',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
**kwargs):
"""Constructor for SignedJwtAssertionCredentials.
@@ -808,6 +915,7 @@ if HAS_CRYPTO:
user_agent: string, HTTP User-Agent to provide for this application.
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
revoke_uri: string, URI for revoke endpoint.
kwargs: kwargs, Additional parameters to add to the JWT token, for
example prn=joe@xample.org."""
@@ -815,6 +923,7 @@ if HAS_CRYPTO:
'http://oauth.net/grant_type/jwt/1.0/bearer',
user_agent=user_agent,
token_uri=token_uri,
revoke_uri=revoke_uri,
)
self.scope = util.scopes_to_string(scope)
@@ -954,8 +1063,10 @@ def _parse_exchange_token_response(content):
@util.positional(4)
def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None, user_agent=None,
token_uri='https://accounts.google.com/o/oauth2/token'):
redirect_uri='postmessage', http=None,
user_agent=None, token_uri=GOOGLE_TOKEN_URI,
auth_uri=GOOGLE_AUTH_URI,
revoke_uri=GOOGLE_REVOKE_URI):
"""Exchanges an authorization code for an OAuth2Credentials object.
Args:
@@ -969,6 +1080,11 @@ def credentials_from_code(client_id, client_secret, scope, code,
http: httplib2.Http, optional http instance to use to do the fetch
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
auth_uri: string, URI for authorization endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Returns:
An OAuth2Credentials object.
@@ -978,8 +1094,8 @@ 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='https://accounts.google.com/o/oauth2/auth',
token_uri=token_uri)
auth_uri=auth_uri, token_uri=token_uri,
revoke_uri=revoke_uri)
credentials = flow.step2_exchange(code, http=http)
return credentials
@@ -1037,8 +1153,9 @@ class OAuth2WebServerFlow(Flow):
def __init__(self, client_id, client_secret, scope,
redirect_uri=None,
user_agent=None,
auth_uri='https://accounts.google.com/o/oauth2/auth',
token_uri='https://accounts.google.com/o/oauth2/token',
auth_uri=GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
**kwargs):
"""Constructor for OAuth2WebServerFlow.
@@ -1059,6 +1176,8 @@ class OAuth2WebServerFlow(Flow):
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
revoke_uri: string, URI for revoke endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
**kwargs: dict, The keyword arguments are all optional and required
parameters for the OAuth calls.
"""
@@ -1069,6 +1188,7 @@ class OAuth2WebServerFlow(Flow):
self.user_agent = user_agent
self.auth_uri = auth_uri
self.token_uri = token_uri
self.revoke_uri = revoke_uri
self.params = {
'access_type': 'offline',
'response_type': 'code',
@@ -1097,16 +1217,13 @@ class OAuth2WebServerFlow(Flow):
if self.redirect_uri is None:
raise ValueError('The value of redirect_uri must not be None.')
query = {
query_params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'scope': self.scope,
}
query.update(self.params)
parts = list(urlparse.urlparse(self.auth_uri))
query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
parts[4] = urllib.urlencode(query)
return urlparse.urlunparse(parts)
query_params.update(self.params)
return _update_query_params(self.auth_uri, query_params)
@util.positional(2)
def step2_exchange(self, code, http=None):
@@ -1172,6 +1289,7 @@ class OAuth2WebServerFlow(Flow):
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))
else:
logger.info('Failed to retrieve access token: %s' % content)
@@ -1184,7 +1302,8 @@ class OAuth2WebServerFlow(Flow):
@util.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None):
"""Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the clientsecrets
@@ -1213,15 +1332,18 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, ca
"""
try:
client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
constructor_kwargs = {
'redirect_uri': redirect_uri,
'auth_uri': client_info['auth_uri'],
'token_uri': client_info['token_uri'],
}
revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None:
constructor_kwargs['revoke_uri'] = revoke_uri
return OAuth2WebServerFlow(
client_info['client_id'],
client_info['client_secret'],
scope,
redirect_uri=redirect_uri,
user_agent=None,
auth_uri=client_info['auth_uri'],
token_uri=client_info['token_uri'])
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)
except clientsecrets.InvalidClientSecretsError:
if message:
@@ -1230,4 +1352,4 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, ca
raise
else:
raise UnknownClientSecretsFlowError(
'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
'This OAuth 2.0 flow is unsupported: %r' % client_type)

View File

@@ -34,11 +34,12 @@ VALID_CLIENT = {
'client_secret',
'redirect_uris',
'auth_uri',
'token_uri'],
'token_uri',
],
'string': [
'client_id',
'client_secret'
]
'client_secret',
],
},
TYPE_INSTALLED: {
'required': [
@@ -46,13 +47,15 @@ VALID_CLIENT = {
'client_secret',
'redirect_uris',
'auth_uri',
'token_uri'],
'token_uri',
],
'string': [
'client_id',
'client_secret'
]
}
}
'client_secret',
],
},
}
class Error(Exception):
"""Base error for this module."""
@@ -144,7 +147,7 @@ def loadfile(filename, cache=None):
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
if obj is None:
client_type, client_info = _loadfile(filename)
obj = { client_type: client_info }
obj = {client_type: client_info}
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
return obj.iteritems().next()

View File

@@ -4,6 +4,7 @@
"client_secret": "foo_client_secret",
"redirect_uris": [],
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token"
"token_uri": "https://accounts.google.com/o/oauth2/token",
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke"
}
}

View File

@@ -57,6 +57,7 @@ from apiclient.http import MediaIoBaseUpload
from apiclient.http import MediaUpload
from apiclient.http import MediaUploadProgress
from apiclient.http import tunnel_patch
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.anyjson import simplejson
from oauth2client.client import OAuth2Credentials
import uritemplate
@@ -896,14 +897,12 @@ class Discovery(unittest.TestCase):
client_secret = 'cOuDdkfjxxnv+'
refresh_token = '1/0/a.df219fjls0'
token_expiry = datetime.datetime.utcnow()
token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
user_agent = 'refresh_checker/1.0'
return OAuth2Credentials(
access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri,
refresh_token, token_expiry, GOOGLE_TOKEN_URI,
user_agent)
def test_pickle_with_credentials(self):
credentials = self._dummy_token()
http = self._dummy_zoo_request()

View File

@@ -29,13 +29,10 @@ import os
import unittest
import urlparse
try:
from urlparse import parse_qs
except ImportError:
from cgi import parse_qs
from apiclient.http import HttpMock
from apiclient.http import HttpMockSequence
from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.anyjson import simplejson
from oauth2client.client import AccessTokenCredentials
from oauth2client.client import AccessTokenCredentialsError
@@ -49,12 +46,17 @@ from oauth2client.client import OAuth2Credentials
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.client import OOB_CALLBACK_URN
from oauth2client.client import REFRESH_STATUS_CODES
from oauth2client.client import Storage
from oauth2client.client import TokenRevokeError
from oauth2client.client import VerifyJwtTokenError
from oauth2client.client import _extract_id_token
from oauth2client.client import _update_query_params
from oauth2client.client import credentials_from_clientsecrets_and_code
from oauth2client.client import credentials_from_code
from oauth2client.client import flow_from_clientsecrets
from oauth2client.clientsecrets import _loadfile
from test_discovery import assertUrisEqual
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
@@ -89,20 +91,54 @@ class CredentialsTests(unittest.TestCase):
restored = Credentials.new_from_json(json)
class DummyDeleteStorage(Storage):
delete_called = False
def locked_delete(self):
self.delete_called = True
def _token_revoke_test_helper(testcase, status, revoke_raise,
valid_bool_value, token_attr):
current_store = getattr(testcase.credentials, 'store', None)
dummy_store = DummyDeleteStorage()
testcase.credentials.set_store(dummy_store)
actual_do_revoke = testcase.credentials._do_revoke
testcase.token_from_revoke = None
def do_revoke_stub(http_request, token):
testcase.token_from_revoke = token
return actual_do_revoke(http_request, token)
testcase.credentials._do_revoke = do_revoke_stub
http = HttpMock(headers={'status': status})
if revoke_raise:
testcase.assertRaises(TokenRevokeError, testcase.credentials.revoke, http)
else:
testcase.credentials.revoke(http)
testcase.assertEqual(getattr(testcase.credentials, token_attr),
testcase.token_from_revoke)
testcase.assertEqual(valid_bool_value, testcase.credentials.invalid)
testcase.assertEqual(valid_bool_value, dummy_store.delete_called)
testcase.credentials.set_store(current_store)
class BasicCredentialsTests(unittest.TestCase):
def setUp(self):
access_token = "foo"
client_id = "some_client_id"
client_secret = "cOuDdkfjxxnv+"
refresh_token = "1/0/a.df219fjls0"
access_token = 'foo'
client_id = 'some_client_id'
client_secret = 'cOuDdkfjxxnv+'
refresh_token = '1/0/a.df219fjls0'
token_expiry = datetime.datetime.utcnow()
token_uri = "https://www.google.com/accounts/o8/oauth2/token"
user_agent = "refresh_checker/1.0"
user_agent = 'refresh_checker/1.0'
self.credentials = OAuth2Credentials(
access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri,
user_agent)
refresh_token, token_expiry, GOOGLE_TOKEN_URI,
user_agent, revoke_uri=GOOGLE_REVOKE_URI)
def test_token_refresh_success(self):
for status_code in REFRESH_STATUS_CODES:
@@ -112,7 +148,7 @@ class BasicCredentialsTests(unittest.TestCase):
({'status': '200'}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
resp, content = http.request("http://example.com")
resp, content = http.request('http://example.com')
self.assertEqual('Bearer 1/3w', content['Authorization'])
self.assertFalse(self.credentials.access_token_expired)
@@ -124,18 +160,28 @@ class BasicCredentialsTests(unittest.TestCase):
])
http = self.credentials.authorize(http)
try:
http.request("http://example.com")
self.fail("should raise AccessTokenRefreshError exception")
http.request('http://example.com')
self.fail('should raise AccessTokenRefreshError exception')
except AccessTokenRefreshError:
pass
self.assertTrue(self.credentials.access_token_expired)
def test_token_revoke_success(self):
_token_revoke_test_helper(
self, '200', revoke_raise=False,
valid_bool_value=True, token_attr='refresh_token')
def test_token_revoke_failure(self):
_token_revoke_test_helper(
self, '400', revoke_raise=True,
valid_bool_value=False, token_attr='refresh_token')
def test_non_401_error_response(self):
http = HttpMockSequence([
({'status': '400'}, ''),
])
http = self.credentials.authorize(http)
resp, content = http.request("http://example.com")
resp, content = http.request('http://example.com')
self.assertEqual(400, resp.status)
def test_to_from_json(self):
@@ -153,17 +199,16 @@ class BasicCredentialsTests(unittest.TestCase):
client_secret = u'cOuDdkfjxxnv+'
refresh_token = u'1/0/a.df219fjls0'
token_expiry = unicode(datetime.datetime.utcnow())
token_uri = u'https://www.google.com/accounts/o8/oauth2/token'
token_uri = unicode(GOOGLE_TOKEN_URI)
revoke_uri = unicode(GOOGLE_REVOKE_URI)
user_agent = u'refresh_checker/1.0'
credentials = OAuth2Credentials(access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri,
user_agent)
user_agent, revoke_uri=revoke_uri)
http = HttpMock(headers={'status': '200'})
http = credentials.authorize(http)
http.request(u'http://example.com', method=u'GET', headers={
u'foo': u'bar'
})
http.request(u'http://example.com', method=u'GET', headers={u'foo': u'bar'})
for k, v in http.headers.iteritems():
self.assertEqual(str, type(k))
self.assertEqual(str, type(v))
@@ -180,9 +225,10 @@ class BasicCredentialsTests(unittest.TestCase):
class AccessTokenCredentialsTests(unittest.TestCase):
def setUp(self):
access_token = "foo"
user_agent = "refresh_checker/1.0"
self.credentials = AccessTokenCredentials(access_token, user_agent)
access_token = 'foo'
user_agent = 'refresh_checker/1.0'
self.credentials = AccessTokenCredentials(access_token, user_agent,
revoke_uri=GOOGLE_REVOKE_URI)
def test_token_refresh_success(self):
for status_code in REFRESH_STATUS_CODES:
@@ -191,12 +237,22 @@ class AccessTokenCredentialsTests(unittest.TestCase):
])
http = self.credentials.authorize(http)
try:
resp, content = http.request("http://example.com")
self.fail("should throw exception if token expires")
resp, content = http.request('http://example.com')
self.fail('should throw exception if token expires')
except AccessTokenCredentialsError:
pass
except Exception:
self.fail("should only throw AccessTokenCredentialsError")
self.fail('should only throw AccessTokenCredentialsError')
def test_token_revoke_success(self):
_token_revoke_test_helper(
self, '200', revoke_raise=False,
valid_bool_value=True, token_attr='access_token')
def test_token_revoke_failure(self):
_token_revoke_test_helper(
self, '400', revoke_raise=True,
valid_bool_value=False, token_attr='access_token')
def test_non_401_error_response(self):
http = HttpMockSequence([
@@ -216,8 +272,8 @@ class AccessTokenCredentialsTests(unittest.TestCase):
class TestAssertionCredentials(unittest.TestCase):
assertion_text = "This is the assertion"
assertion_type = "http://www.google.com/assertionType"
assertion_text = 'This is the assertion'
assertion_type = 'http://www.google.com/assertionType'
class AssertionCredentialsTestImpl(AssertionCredentials):
@@ -225,7 +281,7 @@ class TestAssertionCredentials(unittest.TestCase):
return TestAssertionCredentials.assertion_text
def setUp(self):
user_agent = "fun/2.0"
user_agent = 'fun/2.0'
self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
user_agent=user_agent)
@@ -240,11 +296,34 @@ class TestAssertionCredentials(unittest.TestCase):
({'status': '200'}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
resp, content = http.request("http://example.com")
resp, content = http.request('http://example.com')
self.assertEqual('Bearer 1/3w', content['Authorization'])
def test_token_revoke_success(self):
_token_revoke_test_helper(
self, '200', revoke_raise=False,
valid_bool_value=True, token_attr='access_token')
class ExtractIdTokenText(unittest.TestCase):
def test_token_revoke_failure(self):
_token_revoke_test_helper(
self, '400', revoke_raise=True,
valid_bool_value=False, token_attr='access_token')
class UpdateQueryParamsTest(unittest.TestCase):
def test_update_query_params_no_params(self):
uri = 'http://www.google.com'
updated = _update_query_params(uri, {'a': 'b'})
self.assertEqual(updated, uri + '?a=b')
def test_update_query_params_existing_params(self):
uri = 'http://www.google.com?x=y'
updated = _update_query_params(uri, {'a': 'b', 'c': 'd&'})
hardcoded_update = uri + '&a=b&c=d%26'
assertUrisEqual(self, updated, hardcoded_update)
class ExtractIdTokenTest(unittest.TestCase):
"""Tests _extract_id_token()."""
def test_extract_success(self):
@@ -272,13 +351,14 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
scope='foo',
redirect_uri=OOB_CALLBACK_URN,
user_agent='unittest-sample/1.0',
revoke_uri='dummy_revoke_uri',
)
def test_construct_authorize_url(self):
authorize_url = self.flow.step1_get_authorize_url()
parsed = urlparse.urlparse(authorize_url)
q = parse_qs(parsed[4])
q = urlparse.parse_qs(parsed[4])
self.assertEqual('client_id+1', q['client_id'][0])
self.assertEqual('code', q['response_type'][0])
self.assertEqual('foo', q['scope'][0])
@@ -299,7 +379,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
authorize_url = flow.step1_get_authorize_url()
parsed = urlparse.urlparse(authorize_url)
q = parse_qs(parsed[4])
q = urlparse.parse_qs(parsed[4])
self.assertEqual('client_id+1', q['client_id'][0])
self.assertEqual('token', q['response_type'][0])
self.assertEqual('foo', q['scope'][0])
@@ -313,23 +393,23 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
try:
credentials = self.flow.step2_exchange('some random code', http=http)
self.fail("should raise exception if exchange doesn't get 200")
self.fail('should raise exception if exchange doesn\'t get 200')
except FlowExchangeError:
pass
def test_urlencoded_exchange_failure(self):
http = HttpMockSequence([
({'status': '400'}, "error=invalid_request"),
({'status': '400'}, 'error=invalid_request'),
])
try:
credentials = self.flow.step2_exchange('some random code', http=http)
self.fail("should raise exception if exchange doesn't get 200")
self.fail('should raise exception if exchange doesn\'t get 200')
except FlowExchangeError, e:
self.assertEquals('invalid_request', str(e))
def test_exchange_failure_with_json_error(self):
# Some providers have "error" attribute as a JSON object
# Some providers have 'error' attribute as a JSON object
# in place of regular string.
# This test makes sure no strange object-to-string coversion
# exceptions are being raised instead of FlowExchangeError.
@@ -342,7 +422,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
try:
credentials = self.flow.step2_exchange('some random code', http=http)
self.fail("should raise exception if exchange doesn't get 200")
self.fail('should raise exception if exchange doesn\'t get 200')
except FlowExchangeError, e:
pass
@@ -358,10 +438,11 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
self.assertEqual('SlAV32hkKG', credentials.access_token)
self.assertNotEqual(None, credentials.token_expiry)
self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
self.assertEqual('dummy_revoke_uri', credentials.revoke_uri)
def test_urlencoded_exchange_success(self):
http = HttpMockSequence([
({'status': '200'}, "access_token=SlAV32hkKG&expires_in=3600"),
({'status': '200'}, 'access_token=SlAV32hkKG&expires_in=3600'),
])
credentials = self.flow.step2_exchange('some random code', http=http)
@@ -370,9 +451,9 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_urlencoded_expires_param(self):
http = HttpMockSequence([
# Note the "expires=3600" where you'd normally
# have if named "expires_in"
({'status': '200'}, "access_token=SlAV32hkKG&expires=3600"),
# Note the 'expires=3600' where you'd normally
# have if named 'expires_in'
({'status': '200'}, 'access_token=SlAV32hkKG&expires=3600'),
])
credentials = self.flow.step2_exchange('some random code', http=http)
@@ -391,7 +472,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
http = HttpMockSequence([
# This might be redundant but just to make sure
# urlencoded access_token gets parsed correctly
({'status': '200'}, "access_token=SlAV32hkKG"),
({'status': '200'}, 'access_token=SlAV32hkKG'),
])
credentials = self.flow.step2_exchange('some random code', http=http)
@@ -456,15 +537,15 @@ class CredentialsFromCodeTests(unittest.TestCase):
self.redirect_uri = 'postmessage'
def test_exchange_code_for_token(self):
token = 'asdfghjkl'
payload =simplejson.dumps({'access_token': token, 'expires_in': 3600})
http = HttpMockSequence([
({'status': '200'},
"""{ "access_token":"asdfghjkl",
"expires_in":3600 }"""),
({'status': '200'}, payload),
])
credentials = credentials_from_code(self.client_id, self.client_secret,
self.scope, self.code, redirect_uri=self.redirect_uri,
http=http)
self.assertEquals(credentials.access_token, 'asdfghjkl')
self.assertEquals(credentials.access_token, token)
self.assertNotEqual(None, credentials.token_expiry)
def test_exchange_code_for_token_fail(self):
@@ -476,7 +557,7 @@ class CredentialsFromCodeTests(unittest.TestCase):
credentials = credentials_from_code(self.client_id, self.client_secret,
self.scope, self.code, redirect_uri=self.redirect_uri,
http=http)
self.fail("should raise exception if exchange doesn't get 200")
self.fail('should raise exception if exchange doesn\'t get 200')
except FlowExchangeError:
pass
@@ -513,7 +594,7 @@ class CredentialsFromCodeTests(unittest.TestCase):
credentials = credentials_from_clientsecrets_and_code(
datafile('client_secrets.json'), self.scope,
self.code, http=http)
self.fail("should raise exception if exchange doesn't get 200")
self.fail('should raise exception if exchange doesn\'t get 200')
except FlowExchangeError:
pass

View File

@@ -52,6 +52,7 @@ from google.appengine.ext import ndb
from google.appengine.ext import testbed
from google.appengine.runtime import apiproxy_errors
from oauth2client import appengine
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.anyjson import simplejson
from oauth2client.clientsecrets import _loadfile
from oauth2client.clientsecrets import InvalidClientSecretsError
@@ -156,12 +157,12 @@ class TestAppAssertionCredentials(unittest.TestCase):
def test_raise_correct_type_of_exception(self):
app_identity_stub = self.ErroringAppIdentityStubImpl()
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
app_identity_stub)
apiproxy_stub_map.apiproxy.RegisterStub(
'memcache', memcache_stub.MemcacheServiceStub())
scope = "http://www.googleapis.com/scope"
scope = 'http://www.googleapis.com/scope'
try:
credentials = AppAssertionCredentials(scope)
http = httplib2.Http()
@@ -271,16 +272,15 @@ class StorageByKeyNameTest(unittest.TestCase):
self.testbed.init_memcache_stub()
self.testbed.init_user_stub()
access_token = "foo"
client_id = "some_client_id"
client_secret = "cOuDdkfjxxnv+"
refresh_token = "1/0/a.df219fjls0"
access_token = 'foo'
client_id = 'some_client_id'
client_secret = 'cOuDdkfjxxnv+'
refresh_token = '1/0/a.df219fjls0'
token_expiry = datetime.datetime.utcnow()
token_uri = "https://www.google.com/accounts/o8/oauth2/token"
user_agent = "refresh_checker/1.0"
user_agent = 'refresh_checker/1.0'
self.credentials = OAuth2Credentials(
access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri,
refresh_token, token_expiry, GOOGLE_TOKEN_URI,
user_agent)
def tearDown(self):
@@ -489,7 +489,7 @@ class DecoratorTests(unittest.TestCase):
self.assertEqual(False, self.decorator.has_credentials())
m = mox.Mox()
m.StubOutWithMock(appengine, "_parse_state_value")
m.StubOutWithMock(appengine, '_parse_state_value')
appengine._parse_state_value('foo_path:xsrfkey123',
mox.IgnoreArg()).AndReturn('foo_path')
m.ReplayAll()
@@ -531,7 +531,7 @@ class DecoratorTests(unittest.TestCase):
self.assertTrue(response.status.startswith('302'))
m = mox.Mox()
m.StubOutWithMock(appengine, "_parse_state_value")
m.StubOutWithMock(appengine, '_parse_state_value')
appengine._parse_state_value('foo_path:xsrfkey123',
mox.IgnoreArg()).AndReturn('foo_path')
m.ReplayAll()
@@ -573,7 +573,7 @@ class DecoratorTests(unittest.TestCase):
self.assertEqual('code', q['response_type'][0])
m = mox.Mox()
m.StubOutWithMock(appengine, "_parse_state_value")
m.StubOutWithMock(appengine, '_parse_state_value')
appengine._parse_state_value('bar_path:xsrfkey456',
mox.IgnoreArg()).AndReturn('bar_path')
m.ReplayAll()
@@ -616,7 +616,8 @@ class DecoratorTests(unittest.TestCase):
user_agent='foo_user_agent',
scope=['foo_scope', 'bar_scope'],
access_type='offline',
approval_prompt='force')
approval_prompt='force',
revoke_uri='dummy_revoke_uri')
request_handler = MockRequestHandler()
decorator._create_flow(request_handler)
@@ -625,6 +626,7 @@ class DecoratorTests(unittest.TestCase):
self.assertEqual('offline', decorator.flow.params['access_type'])
self.assertEqual('force', decorator.flow.params['approval_prompt'])
self.assertEqual('foo_user_agent', decorator.flow.user_agent)
self.assertEqual('dummy_revoke_uri', decorator.flow.revoke_uri)
self.assertEqual(None, decorator.flow.params.get('user_agent', None))
def test_decorator_from_client_secrets(self):
@@ -639,6 +641,12 @@ class DecoratorTests(unittest.TestCase):
http = self.decorator.http()
self.assertEquals('foo_access_token', http.request.credentials.access_token)
# revoke_uri is not required
self.assertEqual(self.decorator._revoke_uri,
'https://accounts.google.com/o/oauth2/revoke')
self.assertEqual(self.decorator._revoke_uri,
self.decorator.credentials.revoke_uri)
def test_decorator_from_cached_client_secrets(self):
cache_mock = CacheMock()
load_and_cache('client_secrets.json', 'secret', cache_mock)

View File

@@ -32,6 +32,7 @@ import tempfile
import unittest
from apiclient.http import HttpMockSequence
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import file
from oauth2client import locked_file
from oauth2client import multistore_file

View File

@@ -25,6 +25,7 @@ import keyring
import unittest
import mox
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.client import OAuth2Credentials
from oauth2client.keyring_storage import Storage
@@ -65,12 +66,11 @@ class OAuth2ClientKeyringTests(unittest.TestCase):
client_secret = 'cOuDdkfjxxnv+'
refresh_token = '1/0/a.df219fjls0'
token_expiry = datetime.datetime.utcnow()
token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
user_agent = 'refresh_checker/1.0'
credentials = OAuth2Credentials(
access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri,
refresh_token, token_expiry, GOOGLE_TOKEN_URI,
user_agent)
m = mox.Mox()