Adding a .revoke() to Credentials. Closes issue 98.
Reviewed in https://codereview.appspot.com/7033052/
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
@@ -860,4 +872,4 @@ def oauth2decorator_from_clientsecrets(filename, scope,
|
||||
|
||||
"""
|
||||
return OAuth2DecoratorFromClientSecrets(filename, scope,
|
||||
message=message, cache=cache)
|
||||
message=message, cache=cache)
|
||||
|
||||
@@ -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
|
||||
@@ -405,7 +440,7 @@ class OAuth2Credentials(Credentials):
|
||||
|
||||
Args:
|
||||
http: An instance of httplib2.Http
|
||||
or something that acts like it.
|
||||
or something that acts like it.
|
||||
|
||||
Returns:
|
||||
A modified instance of http that was passed in.
|
||||
@@ -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,16 +824,18 @@ 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.
|
||||
|
||||
Args:
|
||||
assertion_type: string, assertion type that will be declared to the auth
|
||||
server
|
||||
server
|
||||
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.
|
||||
|
||||
@@ -1052,13 +1169,15 @@ class OAuth2WebServerFlow(Flow):
|
||||
scope: string or iterable of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
|
||||
a non-web-based application, or a URI that handles the callback from
|
||||
the authorization server.
|
||||
a non-web-based application, or a URI that handles the callback from
|
||||
the authorization server.
|
||||
user_agent: string, HTTP User-Agent to provide for this application.
|
||||
auth_uri: string, URI for authorization endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||
token_uri: string, URI for token endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||
**kwargs: dict, The keyword arguments are all optional and required
|
||||
parameters for the OAuth calls.
|
||||
"""
|
||||
@@ -1069,10 +1188,11 @@ 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',
|
||||
}
|
||||
}
|
||||
self.params.update(kwargs)
|
||||
|
||||
@util.positional(1)
|
||||
@@ -1081,9 +1201,9 @@ class OAuth2WebServerFlow(Flow):
|
||||
|
||||
Args:
|
||||
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
|
||||
a non-web-based application, or a URI that handles the callback from
|
||||
the authorization server. This parameter is deprecated, please move to
|
||||
passing the redirect_uri in via the constructor.
|
||||
a non-web-based application, or a URI that handles the callback from
|
||||
the authorization server. This parameter is deprecated, please move to
|
||||
passing the redirect_uri in via the constructor.
|
||||
|
||||
Returns:
|
||||
A URI as a string to redirect the user to begin the authorization flow.
|
||||
@@ -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
|
||||
@@ -1194,8 +1313,8 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, ca
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or iterable of strings, scope(s) to request.
|
||||
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
|
||||
a non-web-based application, or a URI that handles the callback from
|
||||
the authorization server.
|
||||
a non-web-based application, or a URI that handles the callback from
|
||||
the authorization server.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. If message is provided then
|
||||
sys.exit will be called in the case of an error. If message in not
|
||||
@@ -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]:
|
||||
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'])
|
||||
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, **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)
|
||||
|
||||
@@ -34,25 +34,28 @@ VALID_CLIENT = {
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri'],
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret'
|
||||
]
|
||||
},
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
TYPE_INSTALLED: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'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."""
|
||||
@@ -123,16 +126,16 @@ def loadfile(filename, cache=None):
|
||||
|
||||
Args:
|
||||
filename: string, Path to a client_secrets.json file on a filesystem.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. If not specified, the file is always being loaded from
|
||||
a filesystem.
|
||||
|
||||
Raises:
|
||||
InvalidClientSecretsError: In case of a validation error or some
|
||||
InvalidClientSecretsError: In case of a validation error or some
|
||||
I/O failure. Can happen only on cache miss.
|
||||
|
||||
Returns:
|
||||
(client_type, client_info) tuple, as _loadfile() normally would.
|
||||
(client_type, client_info) tuple, as _loadfile() normally would.
|
||||
JSON contents is validated only during first load. Cache hits are not
|
||||
validated.
|
||||
"""
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
access_token, client_id, client_secret,
|
||||
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
|
||||
|
||||
@@ -500,8 +581,8 @@ class CredentialsFromCodeTests(unittest.TestCase):
|
||||
load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
|
||||
|
||||
credentials = credentials_from_clientsecrets_and_code(
|
||||
'some_secrets', self.scope,
|
||||
self.code, http=http, cache=cache_mock)
|
||||
'some_secrets', self.scope,
|
||||
self.code, http=http, cache=cache_mock)
|
||||
self.assertEquals(credentials.access_token, 'asdfghjkl')
|
||||
|
||||
def test_exchange_code_and_file_for_token_fail(self):
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user