Add fancy locking to oauth2client.
Reviewed in http://codereview.appspot.com/4919049/
This commit is contained in:
@@ -23,7 +23,6 @@ import httplib2
|
|||||||
import pickle
|
import pickle
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
import logging
|
|
||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
import simplejson
|
import simplejson
|
||||||
@@ -222,7 +221,7 @@ class StorageByKeyName(Storage):
|
|||||||
entity = self._model.get_or_insert(self._key_name)
|
entity = self._model.get_or_insert(self._key_name)
|
||||||
credential = getattr(entity, self._property_name)
|
credential = getattr(entity, self._property_name)
|
||||||
if credential and hasattr(credential, 'set_store'):
|
if credential and hasattr(credential, 'set_store'):
|
||||||
credential.set_store(self.put)
|
credential.set_store(self)
|
||||||
if self._cache:
|
if self._cache:
|
||||||
self._cache.set(self._key_name, pickle.dumps(credentials))
|
self._cache.set(self._key_name, pickle.dumps(credentials))
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""An OAuth 2.0 client
|
"""An OAuth 2.0 client.
|
||||||
|
|
||||||
Tools for interacting with OAuth 2.0 protected
|
Tools for interacting with OAuth 2.0 protected resources.
|
||||||
resources.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
@@ -27,9 +26,9 @@ import logging
|
|||||||
import urllib
|
import urllib
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
import simplejson
|
import simplejson
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
try:
|
try:
|
||||||
# Try to import from django, should work on App Engine
|
# Try to import from django, should work on App Engine
|
||||||
from django.utils import simplejson
|
from django.utils import simplejson
|
||||||
@@ -38,9 +37,11 @@ except ImportError: # pragma: no cover
|
|||||||
import json as simplejson
|
import json as simplejson
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urlparse import parse_qsl
|
from urlparse import parse_qsl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from cgi import parse_qsl
|
from cgi import parse_qsl
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
@@ -92,28 +93,76 @@ class Flow(object):
|
|||||||
class Storage(object):
|
class Storage(object):
|
||||||
"""Base class for all Storage objects.
|
"""Base class for all Storage objects.
|
||||||
|
|
||||||
Store and retrieve a single credential.
|
Store and retrieve a single credential. This class supports locking
|
||||||
|
such that multiple processes and threads can operate on a single
|
||||||
|
store.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self):
|
def acquire_lock(self):
|
||||||
|
"""Acquires any lock necessary to access this Storage.
|
||||||
|
|
||||||
|
This lock is not reentrant."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def release_lock(self):
|
||||||
|
"""Release the Storage lock.
|
||||||
|
|
||||||
|
Trying to release a lock that isn't held will result in a
|
||||||
|
RuntimeError.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def locked_get(self):
|
||||||
"""Retrieve credential.
|
"""Retrieve credential.
|
||||||
|
|
||||||
|
The Storage lock must be held when this is called.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
oauth2client.client.Credentials
|
oauth2client.client.Credentials
|
||||||
"""
|
"""
|
||||||
_abstract()
|
_abstract()
|
||||||
|
|
||||||
def put(self, credentials):
|
def locked_put(self, credentials):
|
||||||
"""Write a credential.
|
"""Write a credential.
|
||||||
|
|
||||||
|
The Storage lock must be held when this is called.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
credentials: Credentials, the credentials to store.
|
credentials: Credentials, the credentials to store.
|
||||||
"""
|
"""
|
||||||
_abstract()
|
_abstract()
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""Retrieve credential.
|
||||||
|
|
||||||
|
The Storage lock must *not* be held when this is called.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
oauth2client.client.Credentials
|
||||||
|
"""
|
||||||
|
self.acquire_lock()
|
||||||
|
try:
|
||||||
|
return self.locked_get()
|
||||||
|
finally:
|
||||||
|
self.release_lock()
|
||||||
|
|
||||||
|
def put(self, credentials):
|
||||||
|
"""Write a credential.
|
||||||
|
|
||||||
|
The Storage lock must be held when this is called.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
credentials: Credentials, the credentials to store.
|
||||||
|
"""
|
||||||
|
self.acquire_lock()
|
||||||
|
try:
|
||||||
|
self.locked_put(credentials)
|
||||||
|
finally:
|
||||||
|
self.release_lock()
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Credentials(Credentials):
|
class OAuth2Credentials(Credentials):
|
||||||
"""Credentials object for OAuth 2.0
|
"""Credentials object for OAuth 2.0.
|
||||||
|
|
||||||
Credentials can be applied to an httplib2.Http object using the authorize()
|
Credentials can be applied to an httplib2.Http object using the authorize()
|
||||||
method, which then signs each request from that object with the OAuth 2.0
|
method, which then signs each request from that object with the OAuth 2.0
|
||||||
@@ -123,22 +172,21 @@ class OAuth2Credentials(Credentials):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
||||||
token_expiry, token_uri, user_agent):
|
token_expiry, token_uri, user_agent):
|
||||||
"""Create an instance of OAuth2Credentials
|
"""Create an instance of OAuth2Credentials.
|
||||||
|
|
||||||
This constructor is not usually called by the user, instead
|
This constructor is not usually called by the user, instead
|
||||||
OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
|
OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token_uri: string, URI of token endpoint.
|
access_token: string, access token.
|
||||||
client_id: string, client identifier.
|
client_id: string, client identifier.
|
||||||
client_secret: string, client secret.
|
client_secret: string, client secret.
|
||||||
access_token: string, access token.
|
|
||||||
token_expiry: datetime, when the access_token expires.
|
|
||||||
refresh_token: string, refresh token.
|
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.
|
user_agent: string, The HTTP User-Agent to provide for this application.
|
||||||
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
store: callable, a callable that when passed a Credential
|
store: callable, a callable that when passed a Credential
|
||||||
will store the credential back to where it came from.
|
will store the credential back to where it came from.
|
||||||
@@ -156,51 +204,66 @@ class OAuth2Credentials(Credentials):
|
|||||||
|
|
||||||
# True if the credentials have been revoked or expired and can't be
|
# True if the credentials have been revoked or expired and can't be
|
||||||
# refreshed.
|
# refreshed.
|
||||||
self._invalid = False
|
self.invalid = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invalid(self):
|
def access_token_expired(self):
|
||||||
"""True if the credentials are invalid, such as being revoked."""
|
"""True if the credential is expired or invalid.
|
||||||
return getattr(self, '_invalid', False)
|
|
||||||
|
If the token_expiry isn't set, we assume the token doesn't expire.
|
||||||
|
"""
|
||||||
|
if self.invalid:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.token_expiry:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
if now >= self.token_expiry:
|
||||||
|
logger.info('access_token is expired. Now: %s, token_expiry: %s',
|
||||||
|
now, self.token_expiry)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def set_store(self, store):
|
def set_store(self, store):
|
||||||
"""Set the storage for the credential.
|
"""Set the Storage for the credential.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
store: callable, a callable that when passed a Credential
|
store: Storage, an implementation of Stroage object.
|
||||||
will store the credential back to where it came from.
|
|
||||||
This is needed to store the latest access_token if it
|
This is needed to store the latest access_token if it
|
||||||
has expired and been refreshed.
|
has expired and been refreshed. This implementation uses
|
||||||
|
locking to check for updates before updating the
|
||||||
|
access_token.
|
||||||
"""
|
"""
|
||||||
self.store = store
|
self.store = store
|
||||||
|
|
||||||
|
def _updateFromCredential(self, other):
|
||||||
|
"""Update this Credential from another instance."""
|
||||||
|
self.__dict__.update(other.__getstate__())
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
"""Trim the state down to something that can be pickled.
|
"""Trim the state down to something that can be pickled."""
|
||||||
"""
|
|
||||||
d = copy.copy(self.__dict__)
|
d = copy.copy(self.__dict__)
|
||||||
del d['store']
|
del d['store']
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
"""Reconstitute the state of the object from being pickled.
|
"""Reconstitute the state of the object from being pickled."""
|
||||||
"""
|
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
self.store = None
|
self.store = None
|
||||||
|
|
||||||
def _generate_refresh_request_body(self):
|
def _generate_refresh_request_body(self):
|
||||||
"""Generate the body that will be used in the refresh request
|
"""Generate the body that will be used in the refresh request."""
|
||||||
"""
|
|
||||||
body = urllib.urlencode({
|
body = urllib.urlencode({
|
||||||
'grant_type': 'refresh_token',
|
'grant_type': 'refresh_token',
|
||||||
'client_id': self.client_id,
|
'client_id': self.client_id,
|
||||||
'client_secret': self.client_secret,
|
'client_secret': self.client_secret,
|
||||||
'refresh_token': self.refresh_token,
|
'refresh_token': self.refresh_token,
|
||||||
})
|
})
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def _generate_refresh_request_headers(self):
|
def _generate_refresh_request_headers(self):
|
||||||
"""Generate the headers that will be used in the refresh request
|
"""Generate the headers that will be used in the refresh request."""
|
||||||
"""
|
|
||||||
headers = {
|
headers = {
|
||||||
'content-type': 'application/x-www-form-urlencoded',
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
}
|
}
|
||||||
@@ -211,16 +274,41 @@ class OAuth2Credentials(Credentials):
|
|||||||
return headers
|
return headers
|
||||||
|
|
||||||
def _refresh(self, http_request):
|
def _refresh(self, http_request):
|
||||||
|
"""Refreshes the access_token.
|
||||||
|
|
||||||
|
This method first checks by reading the Storage object if available.
|
||||||
|
If a refresh is still needed, it holds the Storage lock until the
|
||||||
|
refresh is completed.
|
||||||
|
"""
|
||||||
|
if not self.store:
|
||||||
|
self._do_refresh_request(http_request)
|
||||||
|
else:
|
||||||
|
self.store.acquire_lock()
|
||||||
|
try:
|
||||||
|
new_cred = self.store.locked_get()
|
||||||
|
if (new_cred and not new_cred.invalid and
|
||||||
|
new_cred.access_token != self.access_token):
|
||||||
|
logger.info('Updated access_token read from Storage')
|
||||||
|
self._updateFromCredential(new_cred)
|
||||||
|
else:
|
||||||
|
self._do_refresh_request(http_request)
|
||||||
|
finally:
|
||||||
|
self.store.release_lock()
|
||||||
|
|
||||||
|
def _do_refresh_request(self, http_request):
|
||||||
"""Refresh the access_token using the refresh_token.
|
"""Refresh the access_token using the refresh_token.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
http: An instance of httplib2.Http.request
|
http: An instance of httplib2.Http.request
|
||||||
or something that acts like it.
|
or something that acts like it.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AccessTokenRefreshError: When the refresh fails.
|
||||||
"""
|
"""
|
||||||
body = self._generate_refresh_request_body()
|
body = self._generate_refresh_request_body()
|
||||||
headers = self._generate_refresh_request_headers()
|
headers = self._generate_refresh_request_headers()
|
||||||
|
|
||||||
logging.info("Refresing access_token")
|
logger.info('Refresing access_token')
|
||||||
resp, content = http_request(
|
resp, content = http_request(
|
||||||
self.token_uri, method='POST', body=body, headers=headers)
|
self.token_uri, method='POST', body=body, headers=headers)
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
@@ -233,23 +321,20 @@ class OAuth2Credentials(Credentials):
|
|||||||
seconds=int(d['expires_in'])) + datetime.datetime.now()
|
seconds=int(d['expires_in'])) + datetime.datetime.now()
|
||||||
else:
|
else:
|
||||||
self.token_expiry = None
|
self.token_expiry = None
|
||||||
if self.store is not None:
|
if self.store:
|
||||||
self.store(self)
|
self.store.locked_put(self)
|
||||||
else:
|
else:
|
||||||
# An {'error':...} response body means the token is expired or revoked,
|
# An {'error':...} response body means the token is expired or revoked,
|
||||||
# so we flag the credentials as such.
|
# so we flag the credentials as such.
|
||||||
logging.error('Failed to retrieve access token: %s' % content)
|
logger.error('Failed to retrieve access token: %s' % content)
|
||||||
error_msg = 'Invalid response %s.' % resp['status']
|
error_msg = 'Invalid response %s.' % resp['status']
|
||||||
try:
|
try:
|
||||||
d = simplejson.loads(content)
|
d = simplejson.loads(content)
|
||||||
if 'error' in d:
|
if 'error' in d:
|
||||||
error_msg = d['error']
|
error_msg = d['error']
|
||||||
self._invalid = True
|
self.invalid = True
|
||||||
if self.store is not None:
|
if self.store:
|
||||||
self.store(self)
|
self.store.locked_put(self)
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
"Unable to store refreshed credentials, no Storage provided.")
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
raise AccessTokenRefreshError(error_msg)
|
raise AccessTokenRefreshError(error_msg)
|
||||||
@@ -269,13 +354,11 @@ class OAuth2Credentials(Credentials):
|
|||||||
h = httplib2.Http()
|
h = httplib2.Http()
|
||||||
h = credentials.authorize(h)
|
h = credentials.authorize(h)
|
||||||
|
|
||||||
You can't create a new OAuth
|
You can't create a new OAuth subclass of httplib2.Authenication
|
||||||
subclass of httplib2.Authenication because
|
because it never gets passed the absolute URI, which is needed for
|
||||||
it never gets passed the absolute URI, which is
|
signing. So instead we have to overload 'request' with a closure
|
||||||
needed for signing. So instead we have to overload
|
that adds in the Authorization header and then calls the original
|
||||||
'request' with a closure that adds in the
|
version of 'request()'.
|
||||||
Authorization header and then calls the original version
|
|
||||||
of 'request()'.
|
|
||||||
"""
|
"""
|
||||||
request_orig = http.request
|
request_orig = http.request
|
||||||
|
|
||||||
@@ -284,12 +367,12 @@ class OAuth2Credentials(Credentials):
|
|||||||
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
||||||
connection_type=None):
|
connection_type=None):
|
||||||
if not self.access_token:
|
if not self.access_token:
|
||||||
logging.info("Attempting refresh to obtain initial access_token")
|
logger.info('Attempting refresh to obtain initial access_token')
|
||||||
self._refresh(request_orig)
|
self._refresh(request_orig)
|
||||||
|
|
||||||
"""Modify the request headers to add the appropriate
|
# Modify the request headers to add the appropriate
|
||||||
Authorization header."""
|
# Authorization header.
|
||||||
if headers == None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers['authorization'] = 'OAuth ' + self.access_token
|
headers['authorization'] = 'OAuth ' + self.access_token
|
||||||
|
|
||||||
@@ -303,7 +386,7 @@ class OAuth2Credentials(Credentials):
|
|||||||
redirections, connection_type)
|
redirections, connection_type)
|
||||||
|
|
||||||
if resp.status == 401:
|
if resp.status == 401:
|
||||||
logging.info("Refreshing because we got a 401")
|
logger.info('Refreshing due to a 401')
|
||||||
self._refresh(request_orig)
|
self._refresh(request_orig)
|
||||||
headers['authorization'] = 'OAuth ' + self.access_token
|
headers['authorization'] = 'OAuth ' + self.access_token
|
||||||
return request_orig(uri, method, body, headers,
|
return request_orig(uri, method, body, headers,
|
||||||
@@ -316,14 +399,15 @@ class OAuth2Credentials(Credentials):
|
|||||||
|
|
||||||
|
|
||||||
class AccessTokenCredentials(OAuth2Credentials):
|
class AccessTokenCredentials(OAuth2Credentials):
|
||||||
"""Credentials object for OAuth 2.0
|
"""Credentials object for OAuth 2.0.
|
||||||
|
|
||||||
Credentials can be applied to an httplib2.Http object using the authorize()
|
Credentials can be applied to an httplib2.Http object using the
|
||||||
method, which then signs each request from that object with the OAuth 2.0
|
authorize() method, which then signs each request from that object
|
||||||
access token. This set of credentials is for the use case where you have
|
with the OAuth 2.0 access token. This set of credentials is for the
|
||||||
acquired an OAuth 2.0 access_token from another place such as a JavaScript
|
use case where you have acquired an OAuth 2.0 access_token from
|
||||||
client or another web application, and wish to use it from Python. Because
|
another place such as a JavaScript client or another web
|
||||||
only the access_token is present it can not be refreshed and will in time
|
application, and wish to use it from Python. Because only the
|
||||||
|
access_token is present it can not be refreshed and will in time
|
||||||
expire.
|
expire.
|
||||||
|
|
||||||
AccessTokenCredentials objects may be safely pickled and unpickled.
|
AccessTokenCredentials objects may be safely pickled and unpickled.
|
||||||
@@ -368,19 +452,20 @@ class AccessTokenCredentials(OAuth2Credentials):
|
|||||||
|
|
||||||
|
|
||||||
class AssertionCredentials(OAuth2Credentials):
|
class AssertionCredentials(OAuth2Credentials):
|
||||||
"""Abstract Credentials object used for OAuth 2.0 assertion grants
|
"""Abstract Credentials object used for OAuth 2.0 assertion grants.
|
||||||
|
|
||||||
This credential does not require a flow to instantiate because it represents
|
This credential does not require a flow to instantiate because it
|
||||||
a two legged flow, and therefore has all of the required information to
|
represents a two legged flow, and therefore has all of the required
|
||||||
generate and refresh its own access tokens. It must be subclassed to
|
information to generate and refresh its own access tokens. It must
|
||||||
generate the appropriate assertion string.
|
be subclassed to generate the appropriate assertion string.
|
||||||
|
|
||||||
AssertionCredentials objects may be safely pickled and unpickled.
|
AssertionCredentials objects may be safely pickled and unpickled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, assertion_type, user_agent,
|
def __init__(self, assertion_type, user_agent,
|
||||||
token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
|
token_uri='https://accounts.google.com/o/oauth2/token',
|
||||||
"""Constructor for AssertionFlowCredentials
|
**unused_kwargs):
|
||||||
|
"""Constructor for AssertionFlowCredentials.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
assertion_type: string, assertion type that will be declared to the auth
|
assertion_type: string, assertion type that will be declared to the auth
|
||||||
@@ -403,10 +488,10 @@ class AssertionCredentials(OAuth2Credentials):
|
|||||||
assertion = self._generate_assertion()
|
assertion = self._generate_assertion()
|
||||||
|
|
||||||
body = urllib.urlencode({
|
body = urllib.urlencode({
|
||||||
'assertion_type': self.assertion_type,
|
'assertion_type': self.assertion_type,
|
||||||
'assertion': assertion,
|
'assertion': assertion,
|
||||||
'grant_type': "assertion",
|
'grant_type': 'assertion',
|
||||||
})
|
})
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
@@ -424,10 +509,10 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, client_id, client_secret, scope, user_agent,
|
def __init__(self, client_id, client_secret, scope, user_agent,
|
||||||
auth_uri='https://accounts.google.com/o/oauth2/auth',
|
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',
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Constructor for OAuth2WebServerFlow
|
"""Constructor for OAuth2WebServerFlow.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client_id: string, client identifier.
|
client_id: string, client identifier.
|
||||||
@@ -466,11 +551,11 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
|
|
||||||
self.redirect_uri = redirect_uri
|
self.redirect_uri = redirect_uri
|
||||||
query = {
|
query = {
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'client_id': self.client_id,
|
'client_id': self.client_id,
|
||||||
'redirect_uri': redirect_uri,
|
'redirect_uri': redirect_uri,
|
||||||
'scope': self.scope,
|
'scope': self.scope,
|
||||||
}
|
}
|
||||||
query.update(self.params)
|
query.update(self.params)
|
||||||
parts = list(urlparse.urlparse(self.auth_uri))
|
parts = list(urlparse.urlparse(self.auth_uri))
|
||||||
query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
|
query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
|
||||||
@@ -491,15 +576,16 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
code = code['code']
|
code = code['code']
|
||||||
|
|
||||||
body = urllib.urlencode({
|
body = urllib.urlencode({
|
||||||
'grant_type': 'authorization_code',
|
'grant_type': 'authorization_code',
|
||||||
'client_id': self.client_id,
|
'client_id': self.client_id,
|
||||||
'client_secret': self.client_secret,
|
'client_secret': self.client_secret,
|
||||||
'code': code,
|
'code': code,
|
||||||
'redirect_uri': self.redirect_uri,
|
'redirect_uri': self.redirect_uri,
|
||||||
'scope': self.scope,
|
'scope': self.scope,
|
||||||
})
|
})
|
||||||
headers = {
|
headers = {
|
||||||
'content-type': 'application/x-www-form-urlencoded',
|
'user-agent': self.user_agent,
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.user_agent is not None:
|
if self.user_agent is not None:
|
||||||
@@ -519,12 +605,12 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
token_expiry = datetime.datetime.now() + datetime.timedelta(
|
token_expiry = datetime.datetime.now() + datetime.timedelta(
|
||||||
seconds=int(d['expires_in']))
|
seconds=int(d['expires_in']))
|
||||||
|
|
||||||
logging.info('Successfully retrieved access token: %s' % content)
|
logger.info('Successfully retrieved access token: %s' % content)
|
||||||
return OAuth2Credentials(access_token, self.client_id,
|
return OAuth2Credentials(access_token, self.client_id,
|
||||||
self.client_secret, refresh_token, token_expiry,
|
self.client_secret, refresh_token, token_expiry,
|
||||||
self.token_uri, self.user_agent)
|
self.token_uri, self.user_agent)
|
||||||
else:
|
else:
|
||||||
logging.error('Failed to retrieve access token: %s' % content)
|
logger.error('Failed to retrieve access token: %s' % content)
|
||||||
error_msg = 'Invalid response %s.' % resp['status']
|
error_msg = 'Invalid response %s.' % resp['status']
|
||||||
try:
|
try:
|
||||||
d = simplejson.loads(content)
|
d = simplejson.loads(content)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class Storage(BaseStorage):
|
|||||||
if len(entities) > 0:
|
if len(entities) > 0:
|
||||||
credential = getattr(entities[0], self.property_name)
|
credential = getattr(entities[0], self.property_name)
|
||||||
if credential and hasattr(credential, 'set_store'):
|
if credential and hasattr(credential, 'set_store'):
|
||||||
credential.set_store(self.put)
|
credential.set_store(self)
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
def put(self, credentials):
|
def put(self, credentials):
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Storage(BaseStorage):
|
|||||||
f = open(self._filename, 'r')
|
f = open(self._filename, 'r')
|
||||||
credentials = pickle.loads(f.read())
|
credentials = pickle.loads(f.read())
|
||||||
f.close()
|
f.close()
|
||||||
credentials.set_store(self.put)
|
credentials.set_store(self)
|
||||||
except:
|
except:
|
||||||
credentials = None
|
credentials = None
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
|||||||
361
oauth2client/multistore_file.py
Normal file
361
oauth2client/multistore_file.py
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# Copyright 2011 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
"""Multi-credential file store with lock support.
|
||||||
|
|
||||||
|
This module implements a JSON credential store where multiple
|
||||||
|
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:
|
||||||
|
{
|
||||||
|
'file_version': 1,
|
||||||
|
'data': [
|
||||||
|
{
|
||||||
|
'key': {
|
||||||
|
'clientId': '<client id>',
|
||||||
|
'userAgent': '<user agent>',
|
||||||
|
'scope': '<scope>'
|
||||||
|
},
|
||||||
|
'credential': '<base64 encoding of pickeled Credential object>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = 'jbeda@google.com (Joe Beda)'
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import fcntl
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import threading
|
||||||
|
|
||||||
|
try: # pragma: no cover
|
||||||
|
import simplejson
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
try:
|
||||||
|
# Try to import from django, should work on App Engine
|
||||||
|
from django.utils import simplejson
|
||||||
|
except ImportError:
|
||||||
|
# Should work for Python2.6 and higher.
|
||||||
|
import json as simplejson
|
||||||
|
|
||||||
|
from client import Storage as BaseStorage
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# A dict from 'filename'->_MultiStore instances
|
||||||
|
_multistores = {}
|
||||||
|
_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
|
||||||
|
|
||||||
|
|
||||||
|
def get_credential_storage(filename, client_id, user_agent, scope,
|
||||||
|
warn_on_readonly=True):
|
||||||
|
"""Get a Storage instance for a credential.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The JSON file storing a set of credentials
|
||||||
|
client_id: The client_id for the credential
|
||||||
|
user_agent: The user agent for the credential
|
||||||
|
scope: A string for the scope being requested
|
||||||
|
warn_on_readonly: if True, log a warning if the store is readonly
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An object derived from client.Storage for getting/setting the
|
||||||
|
credential.
|
||||||
|
"""
|
||||||
|
filename = os.path.realpath(os.path.expanduser(filename))
|
||||||
|
_multistores_lock.acquire()
|
||||||
|
try:
|
||||||
|
multistore = _multistores.setdefault(
|
||||||
|
filename, _MultiStore(filename, warn_on_readonly))
|
||||||
|
finally:
|
||||||
|
_multistores_lock.release()
|
||||||
|
return multistore._get_storage(client_id, user_agent, scope)
|
||||||
|
|
||||||
|
|
||||||
|
class _MultiStore(object):
|
||||||
|
"""A file backed store for multiple credentials."""
|
||||||
|
|
||||||
|
def __init__(self, filename, warn_on_readonly=True):
|
||||||
|
"""Initialize the class.
|
||||||
|
|
||||||
|
This will create the file if necessary.
|
||||||
|
"""
|
||||||
|
self._filename = filename
|
||||||
|
self._thread_lock = threading.Lock()
|
||||||
|
self._file_handle = None
|
||||||
|
self._read_only = False
|
||||||
|
self._warn_on_readonly = warn_on_readonly
|
||||||
|
|
||||||
|
self._create_file_if_needed()
|
||||||
|
|
||||||
|
# Cache of deserialized store. This is only valid after the
|
||||||
|
# _MultiStore is locked or _refresh_data_cache is called. This is
|
||||||
|
# of the form of:
|
||||||
|
#
|
||||||
|
# (client_id, user_agent, scope) -> OAuth2Credential
|
||||||
|
#
|
||||||
|
# If this is None, then the store hasn't been read yet.
|
||||||
|
self._data = None
|
||||||
|
|
||||||
|
class _Storage(BaseStorage):
|
||||||
|
"""A Storage object that knows how to read/write a single credential."""
|
||||||
|
|
||||||
|
def __init__(self, multistore, client_id, user_agent, scope):
|
||||||
|
self._multistore = multistore
|
||||||
|
self._client_id = client_id
|
||||||
|
self._user_agent = user_agent
|
||||||
|
self._scope = scope
|
||||||
|
|
||||||
|
def acquire_lock(self):
|
||||||
|
"""Acquires any lock necessary to access this Storage.
|
||||||
|
|
||||||
|
This lock is not reentrant.
|
||||||
|
"""
|
||||||
|
self._multistore._lock()
|
||||||
|
|
||||||
|
def release_lock(self):
|
||||||
|
"""Release the Storage lock.
|
||||||
|
|
||||||
|
Trying to release a lock that isn't held will result in a
|
||||||
|
RuntimeError.
|
||||||
|
"""
|
||||||
|
self._multistore._unlock()
|
||||||
|
|
||||||
|
def locked_get(self):
|
||||||
|
"""Retrieve credential.
|
||||||
|
|
||||||
|
The Storage lock must be held when this is called.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
oauth2client.client.Credentials
|
||||||
|
"""
|
||||||
|
credential = self._multistore._get_credential(
|
||||||
|
self._client_id, self._user_agent, self._scope)
|
||||||
|
if credential:
|
||||||
|
credential.set_store(self)
|
||||||
|
return credential
|
||||||
|
|
||||||
|
def locked_put(self, credentials):
|
||||||
|
"""Write a credential.
|
||||||
|
|
||||||
|
The Storage lock must be held when this is called.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
credentials: Credentials, the credentials to store.
|
||||||
|
"""
|
||||||
|
self._multistore._update_credential(credentials, self._scope)
|
||||||
|
|
||||||
|
def _create_file_if_needed(self):
|
||||||
|
"""Create an empty file if necessary.
|
||||||
|
|
||||||
|
This method will not initialize the file. Instead it implements a
|
||||||
|
simple version of "touch" to ensure the file has been created.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(self._filename):
|
||||||
|
old_umask = os.umask(0177)
|
||||||
|
try:
|
||||||
|
open(self._filename, 'a+').close()
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
|
||||||
|
def _lock(self):
|
||||||
|
"""Lock the entire multistore."""
|
||||||
|
self._thread_lock.acquire()
|
||||||
|
# Check to see if the file is writeable.
|
||||||
|
if os.access(self._filename, os.W_OK):
|
||||||
|
self._file_handle = open(self._filename, 'r+')
|
||||||
|
fcntl.lockf(self._file_handle.fileno(), fcntl.LOCK_EX)
|
||||||
|
else:
|
||||||
|
# Cannot open in read/write mode. Open only in read mode.
|
||||||
|
self._file_handle = open(self._filename, 'r')
|
||||||
|
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._filename)
|
||||||
|
if os.path.getsize(self._filename) == 0:
|
||||||
|
logger.debug('Initializing empty multistore file')
|
||||||
|
# The multistore is empty so write out an empty file.
|
||||||
|
self._data = {}
|
||||||
|
self._write()
|
||||||
|
elif not self._read_only or self._data is None:
|
||||||
|
# Only refresh the data if we are read/write or we haven't
|
||||||
|
# cached the data yet. If we are readonly, we assume is isn't
|
||||||
|
# changing out from under us and that we only have to read it
|
||||||
|
# once. This prevents us from whacking any new access keys that
|
||||||
|
# we have cached in memory but were unable to write out.
|
||||||
|
self._refresh_data_cache()
|
||||||
|
|
||||||
|
def _unlock(self):
|
||||||
|
"""Release the lock on the multistore."""
|
||||||
|
if not self._read_only:
|
||||||
|
fcntl.lockf(self._file_handle.fileno(), fcntl.LOCK_UN)
|
||||||
|
self._file_handle.close()
|
||||||
|
self._thread_lock.release()
|
||||||
|
|
||||||
|
def _locked_json_read(self):
|
||||||
|
"""Get the raw content of the multistore file.
|
||||||
|
|
||||||
|
The multistore must be locked when this is called.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The contents of the multistore decoded as JSON.
|
||||||
|
"""
|
||||||
|
assert self._thread_lock.locked()
|
||||||
|
self._file_handle.seek(0)
|
||||||
|
return simplejson.load(self._file_handle)
|
||||||
|
|
||||||
|
def _locked_json_write(self, data):
|
||||||
|
"""Write a JSON serializable data structure to the multistore.
|
||||||
|
|
||||||
|
The multistore must be locked when this is called.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The data to be serialized and written.
|
||||||
|
"""
|
||||||
|
assert self._thread_lock.locked()
|
||||||
|
if self._read_only:
|
||||||
|
return
|
||||||
|
self._file_handle.seek(0)
|
||||||
|
simplejson.dump(data, self._file_handle, sort_keys=True, indent=2)
|
||||||
|
self._file_handle.truncate()
|
||||||
|
|
||||||
|
def _refresh_data_cache(self):
|
||||||
|
"""Refresh the contents of the multistore.
|
||||||
|
|
||||||
|
The multistore must be locked when this is called.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NewerCredentialStoreError: Raised when a newer client has written the
|
||||||
|
store.
|
||||||
|
"""
|
||||||
|
self._data = {}
|
||||||
|
try:
|
||||||
|
raw_data = self._locked_json_read()
|
||||||
|
except Exception:
|
||||||
|
logger.warn('Credential data store could not be loaded. '
|
||||||
|
'Will ignore and overwrite.')
|
||||||
|
return
|
||||||
|
|
||||||
|
version = 0
|
||||||
|
try:
|
||||||
|
version = raw_data['file_version']
|
||||||
|
except Exception:
|
||||||
|
logger.warn('Missing version for credential data store. It may be '
|
||||||
|
'corrupt or an old version. Overwriting.')
|
||||||
|
if version > 1:
|
||||||
|
raise NewerCredentialStoreError(
|
||||||
|
'Credential file has file_version of %d. '
|
||||||
|
'Only file_version of 1 is supported.' % version)
|
||||||
|
|
||||||
|
credentials = []
|
||||||
|
try:
|
||||||
|
credentials = raw_data['data']
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for cred_entry in credentials:
|
||||||
|
try:
|
||||||
|
(key, credential) = self._decode_credential_from_json(cred_entry)
|
||||||
|
self._data[key] = credential
|
||||||
|
except:
|
||||||
|
# If something goes wrong loading a credential, just ignore it
|
||||||
|
logger.info('Error decoding credential, skipping', exc_info=True)
|
||||||
|
|
||||||
|
def _decode_credential_from_json(self, cred_entry):
|
||||||
|
"""Load a credential from our JSON serialization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cred_entry: A dict entry from the data member of our format
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(key, cred) where the key is the key tuple and the cred is the
|
||||||
|
OAuth2Credential object.
|
||||||
|
"""
|
||||||
|
raw_key = cred_entry['key']
|
||||||
|
client_id = raw_key['clientId']
|
||||||
|
user_agent = raw_key['userAgent']
|
||||||
|
scope = raw_key['scope']
|
||||||
|
key = (client_id, user_agent, scope)
|
||||||
|
credential = pickle.loads(base64.b64decode(cred_entry['credential']))
|
||||||
|
return (key, credential)
|
||||||
|
|
||||||
|
def _write(self):
|
||||||
|
"""Write the cached data back out.
|
||||||
|
|
||||||
|
The multistore must be locked.
|
||||||
|
"""
|
||||||
|
raw_data = {'file_version': 1}
|
||||||
|
raw_creds = []
|
||||||
|
raw_data['data'] = raw_creds
|
||||||
|
for (cred_key, cred) in self._data.items():
|
||||||
|
raw_key = {
|
||||||
|
'clientId': cred_key[0],
|
||||||
|
'userAgent': cred_key[1],
|
||||||
|
'scope': cred_key[2]
|
||||||
|
}
|
||||||
|
raw_cred = base64.b64encode(pickle.dumps(cred))
|
||||||
|
raw_creds.append({'key': raw_key, 'credential': raw_cred})
|
||||||
|
self._locked_json_write(raw_data)
|
||||||
|
|
||||||
|
def _get_credential(self, client_id, user_agent, scope):
|
||||||
|
"""Get a credential from the multistore.
|
||||||
|
|
||||||
|
The multistore must be locked.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client_id: The client_id for the credential
|
||||||
|
user_agent: The user agent for the credential
|
||||||
|
scope: A string for the scope being requested
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The credential specified or None if not present
|
||||||
|
"""
|
||||||
|
key = (client_id, user_agent, scope)
|
||||||
|
return self._data.get(key, None)
|
||||||
|
|
||||||
|
def _update_credential(self, cred, scope):
|
||||||
|
"""Update a credential and write the multistore.
|
||||||
|
|
||||||
|
This must be called when the multistore is locked.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cred: The OAuth2Credential to update/set
|
||||||
|
scope: The scope that this credential covers
|
||||||
|
"""
|
||||||
|
key = (cred.client_id, cred.user_agent, scope)
|
||||||
|
self._data[key] = cred
|
||||||
|
self._write()
|
||||||
|
|
||||||
|
def _get_storage(self, client_id, user_agent, scope):
|
||||||
|
"""Get a Storage object to get/set a credential.
|
||||||
|
|
||||||
|
This Storage is a 'view' into the multistore.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client_id: The client_id for the credential
|
||||||
|
user_agent: The user agent for the credential
|
||||||
|
scope: A string for the scope being requested
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Storage object that can be used to get/set this cred
|
||||||
|
"""
|
||||||
|
return self._Storage(self, client_id, user_agent, scope)
|
||||||
@@ -25,31 +25,30 @@ __all__ = ['run']
|
|||||||
|
|
||||||
import BaseHTTPServer
|
import BaseHTTPServer
|
||||||
import gflags
|
import gflags
|
||||||
import logging
|
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from client import FlowExchangeError
|
from client import FlowExchangeError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urlparse import parse_qsl
|
from urlparse import parse_qsl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from cgi import parse_qsl
|
from cgi import parse_qsl
|
||||||
|
|
||||||
|
|
||||||
FLAGS = gflags.FLAGS
|
FLAGS = gflags.FLAGS
|
||||||
|
|
||||||
gflags.DEFINE_boolean('auth_local_webserver', True,
|
gflags.DEFINE_boolean('auth_local_webserver', True,
|
||||||
('Run a local web server to handle redirects during '
|
('Run a local web server to handle redirects during '
|
||||||
'OAuth authorization.'))
|
'OAuth authorization.'))
|
||||||
|
|
||||||
gflags.DEFINE_string('auth_host_name', 'localhost',
|
gflags.DEFINE_string('auth_host_name', 'localhost',
|
||||||
('Host name to use when running a local web server to '
|
('Host name to use when running a local web server to '
|
||||||
'handle redirects during OAuth authorization.'))
|
'handle redirects during OAuth authorization.'))
|
||||||
|
|
||||||
gflags.DEFINE_multi_int('auth_host_port', [8080, 8090],
|
gflags.DEFINE_multi_int('auth_host_port', [8080, 8090],
|
||||||
('Port to use when running a local web server to '
|
('Port to use when running a local web server to '
|
||||||
'handle redirects during OAuth authorization.'))
|
'handle redirects during OAuth authorization.'))
|
||||||
|
|
||||||
|
|
||||||
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
|
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
|
||||||
@@ -69,7 +68,7 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def do_GET(s):
|
def do_GET(s):
|
||||||
"""Handle a GET request
|
"""Handle a GET request.
|
||||||
|
|
||||||
Parses the query parameters and prints a message
|
Parses the query parameters and prints a message
|
||||||
if the flow has completed. Note that we can't detect
|
if the flow has completed. Note that we can't detect
|
||||||
@@ -106,8 +105,8 @@ def run(flow, storage):
|
|||||||
for port in FLAGS.auth_host_port:
|
for port in FLAGS.auth_host_port:
|
||||||
port_number = port
|
port_number = port
|
||||||
try:
|
try:
|
||||||
httpd = BaseHTTPServer.HTTPServer((FLAGS.auth_host_name, port),
|
httpd = ClientRedirectServer((FLAGS.auth_host_name, port),
|
||||||
ClientRedirectHandler)
|
ClientRedirectHandler)
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -126,10 +125,10 @@ def run(flow, storage):
|
|||||||
print
|
print
|
||||||
if FLAGS.auth_local_webserver:
|
if FLAGS.auth_local_webserver:
|
||||||
print 'If your browser is on a different machine then exit and re-run this'
|
print 'If your browser is on a different machine then exit and re-run this'
|
||||||
print 'application with the command-line parameter --noauth_local_webserver.'
|
print 'application with the command-line parameter '
|
||||||
|
print '--noauth_local_webserver.'
|
||||||
print
|
print
|
||||||
|
|
||||||
|
|
||||||
if FLAGS.auth_local_webserver:
|
if FLAGS.auth_local_webserver:
|
||||||
httpd.handle_request()
|
httpd.handle_request()
|
||||||
if 'error' in httpd.query_params:
|
if 'error' in httpd.query_params:
|
||||||
@@ -137,18 +136,15 @@ def run(flow, storage):
|
|||||||
if 'code' in httpd.query_params:
|
if 'code' in httpd.query_params:
|
||||||
code = httpd.query_params['code']
|
code = httpd.query_params['code']
|
||||||
else:
|
else:
|
||||||
accepted = 'n'
|
code = raw_input('Enter verification code: ').strip()
|
||||||
while accepted.lower() == 'n':
|
|
||||||
accepted = raw_input('Have you authorized me? (y/n) ')
|
|
||||||
code = raw_input('What is the verification code? ').strip()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
credentials = flow.step2_exchange(code)
|
credential = flow.step2_exchange(code)
|
||||||
except FlowExchangeError:
|
except FlowExchangeError, e:
|
||||||
sys.exit('The authentication has failed.')
|
sys.exit('Authentication has failed: %s' % e)
|
||||||
|
|
||||||
storage.put(credentials)
|
storage.put(credential)
|
||||||
credentials.set_store(storage.put)
|
credential.set_store(storage)
|
||||||
print "You have successfully authenticated."
|
print 'Authentication successful.'
|
||||||
|
|
||||||
return credentials
|
return credential
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ class DecoratorTests(unittest.TestCase):
|
|||||||
self.decorator.credentials.access_token)
|
self.decorator.credentials.access_token)
|
||||||
|
|
||||||
# Invalidate the stored Credentials
|
# Invalidate the stored Credentials
|
||||||
self.decorator.credentials._invalid = True
|
self.decorator.credentials.invalid = True
|
||||||
self.decorator.credentials.store(self.decorator.credentials)
|
self.decorator.credentials.store.put(self.decorator.credentials)
|
||||||
|
|
||||||
# Invalid Credentials should start the OAuth dance again
|
# Invalid Credentials should start the OAuth dance again
|
||||||
response = self.app.get('/foo_path')
|
response = self.app.get('/foo_path')
|
||||||
|
|||||||
Reference in New Issue
Block a user