+++ /dev/null
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-Example consumer. This is not recommended for production.
-Instead, you'll want to create your own subclass of OAuthClient
-or find one that works with your web framework.
-"""
-
-import httplib
-import time
-import oauth.oauth as oauth
-
-# settings for the local test consumer
-SERVER = 'localhost'
-PORT = 8080
-
-# fake urls for the test server (matches ones in server.py)
-REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
-ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
-AUTHORIZATION_URL = 'https://photos.example.net/authorize'
-CALLBACK_URL = 'http://printer.example.com/request_token_ready'
-RESOURCE_URL = 'http://photos.example.net/photos'
-
-# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore
-CONSUMER_KEY = 'key'
-CONSUMER_SECRET = 'secret'
-
-# example client using httplib with headers
-class SimpleOAuthClient(oauth.OAuthClient):
-
- def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', access_token_url='', authorization_url=''):
- self.server = server
- self.port = port
- self.request_token_url = request_token_url
- self.access_token_url = access_token_url
- self.authorization_url = authorization_url
- self.connection = httplib.HTTPConnection("%s:%d" % (self.server, self.port))
-
- def fetch_request_token(self, oauth_request):
- # via headers
- # -> OAuthToken
- self.connection.request(oauth_request.http_method, self.request_token_url, headers=oauth_request.to_header())
- response = self.connection.getresponse()
- return oauth.OAuthToken.from_string(response.read())
-
- def fetch_access_token(self, oauth_request):
- # via headers
- # -> OAuthToken
- self.connection.request(oauth_request.http_method, self.access_token_url, headers=oauth_request.to_header())
- response = self.connection.getresponse()
- return oauth.OAuthToken.from_string(response.read())
-
- def authorize_token(self, oauth_request):
- # via url
- # -> typically just some okay response
- self.connection.request(oauth_request.http_method, oauth_request.to_url())
- response = self.connection.getresponse()
- return response.read()
-
- def access_resource(self, oauth_request):
- # via post body
- # -> some protected resources
- headers = {'Content-Type' :'application/x-www-form-urlencoded'}
- self.connection.request('POST', RESOURCE_URL, body=oauth_request.to_postdata(), headers=headers)
- response = self.connection.getresponse()
- return response.read()
-
-def run_example():
-
- # setup
- print '** OAuth Python Library Example **'
- client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZATION_URL)
- consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET)
- signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT()
- signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
- pause()
-
- # get request token
- print '* Obtain a request token ...'
- pause()
- oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, callback=CALLBACK_URL, http_url=client.request_token_url)
- oauth_request.sign_request(signature_method_plaintext, consumer, None)
- print 'REQUEST (via headers)'
- print 'parameters: %s' % str(oauth_request.parameters)
- pause()
- token = client.fetch_request_token(oauth_request)
- print 'GOT'
- print 'key: %s' % str(token.key)
- print 'secret: %s' % str(token.secret)
- print 'callback confirmed? %s' % str(token.callback_confirmed)
- pause()
-
- print '* Authorize the request token ...'
- pause()
- oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=client.authorization_url)
- print 'REQUEST (via url query string)'
- print 'parameters: %s' % str(oauth_request.parameters)
- pause()
- # this will actually occur only on some callback
- response = client.authorize_token(oauth_request)
- print 'GOT'
- print response
- # sad way to get the verifier
- import urlparse, cgi
- query = urlparse.urlparse(response)[4]
- params = cgi.parse_qs(query, keep_blank_values=False)
- verifier = params['oauth_verifier'][0]
- print 'verifier: %s' % verifier
- pause()
-
- # get access token
- print '* Obtain an access token ...'
- pause()
- oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, verifier=verifier, http_url=client.access_token_url)
- oauth_request.sign_request(signature_method_plaintext, consumer, token)
- print 'REQUEST (via headers)'
- print 'parameters: %s' % str(oauth_request.parameters)
- pause()
- token = client.fetch_access_token(oauth_request)
- print 'GOT'
- print 'key: %s' % str(token.key)
- print 'secret: %s' % str(token.secret)
- pause()
-
- # access some protected resources
- print '* Access protected resources ...'
- pause()
- parameters = {'file': 'vacation.jpg', 'size': 'original'} # resource specific params
- oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_method='POST', http_url=RESOURCE_URL, parameters=parameters)
- oauth_request.sign_request(signature_method_hmac_sha1, consumer, token)
- print 'REQUEST (via post body)'
- print 'parameters: %s' % str(oauth_request.parameters)
- pause()
- params = client.access_resource(oauth_request)
- print 'GOT'
- print 'non-oauth parameters: %s' % params
- pause()
-
-def pause():
- print ''
- time.sleep(1)
-
-if __name__ == '__main__':
- run_example()
- print 'Done.'
\ No newline at end of file
+++ /dev/null
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
-import urllib
-
-import oauth.oauth as oauth
-
-# fake urls for the test server
-REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
-ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
-AUTHORIZATION_URL = 'https://photos.example.net/authorize'
-CALLBACK_URL = 'http://printer.example.com/request_token_ready'
-RESOURCE_URL = 'http://photos.example.net/photos'
-REALM = 'http://photos.example.net/'
-VERIFIER = 'verifier'
-
-# example store for one of each thing
-class MockOAuthDataStore(oauth.OAuthDataStore):
-
- def __init__(self):
- self.consumer = oauth.OAuthConsumer('key', 'secret')
- self.request_token = oauth.OAuthToken('requestkey', 'requestsecret')
- self.access_token = oauth.OAuthToken('accesskey', 'accesssecret')
- self.nonce = 'nonce'
- self.verifier = VERIFIER
-
- def lookup_consumer(self, key):
- if key == self.consumer.key:
- return self.consumer
- return None
-
- def lookup_token(self, token_type, token):
- token_attrib = getattr(self, '%s_token' % token_type)
- if token == token_attrib.key:
- ## HACK
- token_attrib.set_callback(CALLBACK_URL)
- return token_attrib
- return None
-
- def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
- if oauth_token and oauth_consumer.key == self.consumer.key and (oauth_token.key == self.request_token.key or oauth_token.key == self.access_token.key) and nonce == self.nonce:
- return self.nonce
- return None
-
- def fetch_request_token(self, oauth_consumer, oauth_callback):
- if oauth_consumer.key == self.consumer.key:
- if oauth_callback:
- # want to check here if callback is sensible
- # for mock store, we assume it is
- self.request_token.set_callback(oauth_callback)
- return self.request_token
- return None
-
- def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
- if oauth_consumer.key == self.consumer.key and oauth_token.key == self.request_token.key and oauth_verifier == self.verifier:
- # want to check here if token is authorized
- # for mock store, we assume it is
- return self.access_token
- return None
-
- def authorize_request_token(self, oauth_token, user):
- if oauth_token.key == self.request_token.key:
- # authorize the request token in the store
- # for mock store, do nothing
- return self.request_token
- return None
-
-class RequestHandler(BaseHTTPRequestHandler):
-
- def __init__(self, *args, **kwargs):
- self.oauth_server = oauth.OAuthServer(MockOAuthDataStore())
- self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
- self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
- BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
-
- # example way to send an oauth error
- def send_oauth_error(self, err=None):
- # send a 401 error
- self.send_error(401, str(err.message))
- # return the authenticate header
- header = oauth.build_authenticate_header(realm=REALM)
- for k, v in header.iteritems():
- self.send_header(k, v)
-
- def do_GET(self):
-
- # debug info
- #print self.command, self.path, self.headers
-
- # get the post data (if any)
- postdata = None
- if self.command == 'POST':
- try:
- length = int(self.headers.getheader('content-length'))
- postdata = self.rfile.read(length)
- except:
- pass
-
- # construct the oauth request from the request parameters
- oauth_request = oauth.OAuthRequest.from_request(self.command, self.path, headers=self.headers, query_string=postdata)
-
- # request token
- if self.path.startswith(REQUEST_TOKEN_URL):
- try:
- # create a request token
- token = self.oauth_server.fetch_request_token(oauth_request)
- # send okay response
- self.send_response(200, 'OK')
- self.end_headers()
- # return the token
- self.wfile.write(token.to_string())
- except oauth.OAuthError, err:
- self.send_oauth_error(err)
- return
-
- # user authorization
- if self.path.startswith(AUTHORIZATION_URL):
- try:
- # get the request token
- token = self.oauth_server.fetch_request_token(oauth_request)
- # authorize the token (kind of does nothing for now)
- token = self.oauth_server.authorize_token(token, None)
- token.set_verifier(VERIFIER)
- # send okay response
- self.send_response(200, 'OK')
- self.end_headers()
- # return the callback url (to show server has it)
- self.wfile.write(token.get_callback_url())
- except oauth.OAuthError, err:
- self.send_oauth_error(err)
- return
-
- # access token
- if self.path.startswith(ACCESS_TOKEN_URL):
- try:
- # create an access token
- token = self.oauth_server.fetch_access_token(oauth_request)
- # send okay response
- self.send_response(200, 'OK')
- self.end_headers()
- # return the token
- self.wfile.write(token.to_string())
- except oauth.OAuthError, err:
- self.send_oauth_error(err)
- return
-
- # protected resources
- if self.path.startswith(RESOURCE_URL):
- try:
- # verify the request has been oauth authorized
- consumer, token, params = self.oauth_server.verify_request(oauth_request)
- # send okay response
- self.send_response(200, 'OK')
- self.end_headers()
- # return the extra parameters - just for something to return
- self.wfile.write(str(params))
- except oauth.OAuthError, err:
- self.send_oauth_error(err)
- return
-
- def do_POST(self):
- return self.do_GET()
-
-def main():
- try:
- server = HTTPServer(('', 8080), RequestHandler)
- print 'Test server running...'
- server.serve_forever()
- except KeyboardInterrupt:
- server.socket.close()
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
+++ /dev/null
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-
-class OAuthError(RuntimeError):
- """Generic exception class."""
- def __init__(self, message='OAuth error occured.'):
- self.message = message
-
-def build_authenticate_header(realm=''):
- """Optional WWW-Authenticate header (401 error)"""
- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-def escape(s):
- """Escape a URL including any /."""
- return urllib.quote(s, safe='~')
-
-def _utf8_str(s):
- """Convert unicode to utf-8."""
- if isinstance(s, unicode):
- return s.encode("utf-8")
- else:
- return str(s)
-
-def generate_timestamp():
- """Get seconds since epoch (UTC)."""
- return int(time.time())
-
-def generate_nonce(length=8):
- """Generate pseudorandom number."""
- return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-def generate_verifier(length=8):
- """Generate pseudorandom number."""
- return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-class OAuthConsumer(object):
- """Consumer of OAuth authentication.
-
- OAuthConsumer is a data type that represents the identity of the Consumer
- via its shared secret with the Service Provider.
-
- """
- key = None
- secret = None
-
- def __init__(self, key, secret):
- self.key = key
- self.secret = secret
-
-
-class OAuthToken(object):
- """OAuthToken is a data type that represents an End User via either an access
- or request token.
-
- key -- the token
- secret -- the token secret
-
- """
- key = None
- secret = None
- callback = None
- callback_confirmed = None
- verifier = None
-
- def __init__(self, key, secret):
- self.key = key
- self.secret = secret
-
- def set_callback(self, callback):
- self.callback = callback
- self.callback_confirmed = 'true'
-
- def set_verifier(self, verifier=None):
- if verifier is not None:
- self.verifier = verifier
- else:
- self.verifier = generate_verifier()
-
- def get_callback_url(self):
- if self.callback and self.verifier:
- # Append the oauth_verifier.
- parts = urlparse.urlparse(self.callback)
- scheme, netloc, path, params, query, fragment = parts[:6]
- if query:
- query = '%s&oauth_verifier=%s' % (query, self.verifier)
- else:
- query = 'oauth_verifier=%s' % self.verifier
- return urlparse.urlunparse((scheme, netloc, path, params,
- query, fragment))
- return self.callback
-
- def to_string(self):
- data = {
- 'oauth_token': self.key,
- 'oauth_token_secret': self.secret,
- }
- if self.callback_confirmed is not None:
- data['oauth_callback_confirmed'] = self.callback_confirmed
- return urllib.urlencode(data)
-
- def from_string(s):
- """ Returns a token from something like:
- oauth_token_secret=xxx&oauth_token=xxx
- """
- params = cgi.parse_qs(s, keep_blank_values=False)
- key = params['oauth_token'][0]
- secret = params['oauth_token_secret'][0]
- token = OAuthToken(key, secret)
- try:
- token.callback_confirmed = params['oauth_callback_confirmed'][0]
- except KeyError:
- pass # 1.0, no callback confirmed.
- return token
- from_string = staticmethod(from_string)
-
- def __str__(self):
- return self.to_string()
-
-
-class OAuthRequest(object):
- """OAuthRequest represents the request and can be serialized.
-
- OAuth parameters:
- - oauth_consumer_key
- - oauth_token
- - oauth_signature_method
- - oauth_signature
- - oauth_timestamp
- - oauth_nonce
- - oauth_version
- - oauth_verifier
- ... any additional parameters, as defined by the Service Provider.
- """
- parameters = None # OAuth parameters.
- http_method = HTTP_METHOD
- http_url = None
- version = VERSION
-
- def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
- self.http_method = http_method
- self.http_url = http_url
- self.parameters = parameters or {}
-
- def set_parameter(self, parameter, value):
- self.parameters[parameter] = value
-
- def get_parameter(self, parameter):
- try:
- return self.parameters[parameter]
- except:
- raise OAuthError('Parameter not found: %s' % parameter)
-
- def _get_timestamp_nonce(self):
- return self.get_parameter('oauth_timestamp'), self.get_parameter(
- 'oauth_nonce')
-
- def get_nonoauth_parameters(self):
- """Get any non-OAuth parameters."""
- parameters = {}
- for k, v in self.parameters.iteritems():
- # Ignore oauth parameters.
- if k.find('oauth_') < 0:
- parameters[k] = v
- return parameters
-
- def to_header(self, realm=''):
- """Serialize as a header for an HTTPAuth request."""
- auth_header = 'OAuth realm="%s"' % realm
- # Add the oauth parameters.
- if self.parameters:
- for k, v in self.parameters.iteritems():
- if k[:6] == 'oauth_':
- auth_header += ', %s="%s"' % (k, escape(str(v)))
- return {'Authorization': auth_header}
-
- def to_postdata(self):
- """Serialize as post data for a POST request."""
- return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
- for k, v in self.parameters.iteritems()])
-
- def to_url(self):
- """Serialize as a URL for a GET request."""
- return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
-
- def get_normalized_parameters(self):
- """Return a string that contains the parameters that must be signed."""
- params = self.parameters
- try:
- # Exclude the signature if it exists.
- del params['oauth_signature']
- except:
- pass
- # Escape key values before sorting.
- key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
- for k,v in params.items()]
- # Sort lexicographically, first after key, then after value.
- key_values.sort()
- # Combine key value pairs into a string.
- return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
-
- def get_normalized_http_method(self):
- """Uppercases the http method."""
- return self.http_method.upper()
-
- def get_normalized_http_url(self):
- """Parses the URL and rebuilds it to be scheme://host/path."""
- parts = urlparse.urlparse(self.http_url)
- scheme, netloc, path = parts[:3]
- # Exclude default port numbers.
- if scheme == 'http' and netloc[-3:] == ':80':
- netloc = netloc[:-3]
- elif scheme == 'https' and netloc[-4:] == ':443':
- netloc = netloc[:-4]
- return '%s://%s%s' % (scheme, netloc, path)
-
- def sign_request(self, signature_method, consumer, token):
- """Set the signature parameter to the result of build_signature."""
- # Set the signature method.
- self.set_parameter('oauth_signature_method',
- signature_method.get_name())
- # Set the signature.
- self.set_parameter('oauth_signature',
- self.build_signature(signature_method, consumer, token))
-
- def build_signature(self, signature_method, consumer, token):
- """Calls the build signature method within the signature method."""
- return signature_method.build_signature(self, consumer, token)
-
- def from_request(http_method, http_url, headers=None, parameters=None,
- query_string=None):
- """Combines multiple parameter sources."""
- if parameters is None:
- parameters = {}
-
- # Headers
- if headers and 'Authorization' in headers:
- auth_header = headers['Authorization']
- # Check that the authorization header is OAuth.
- if auth_header[:6] == 'OAuth ':
- auth_header = auth_header[6:]
- try:
- # Get the parameters from the header.
- header_params = OAuthRequest._split_header(auth_header)
- parameters.update(header_params)
- except:
- raise OAuthError('Unable to parse OAuth parameters from '
- 'Authorization header.')
-
- # GET or POST query string.
- if query_string:
- query_params = OAuthRequest._split_url_string(query_string)
- parameters.update(query_params)
-
- # URL parameters.
- param_str = urlparse.urlparse(http_url)[4] # query
- url_params = OAuthRequest._split_url_string(param_str)
- parameters.update(url_params)
-
- if parameters:
- return OAuthRequest(http_method, http_url, parameters)
-
- return None
- from_request = staticmethod(from_request)
-
- def from_consumer_and_token(oauth_consumer, token=None,
- callback=None, verifier=None, http_method=HTTP_METHOD,
- http_url=None, parameters=None):
- if not parameters:
- parameters = {}
-
- defaults = {
- 'oauth_consumer_key': oauth_consumer.key,
- 'oauth_timestamp': generate_timestamp(),
- 'oauth_nonce': generate_nonce(),
- 'oauth_version': OAuthRequest.version,
- }
-
- defaults.update(parameters)
- parameters = defaults
-
- if token:
- parameters['oauth_token'] = token.key
- if token.callback:
- parameters['oauth_callback'] = token.callback
- # 1.0a support for verifier.
- if verifier:
- parameters['oauth_verifier'] = verifier
- elif callback:
- # 1.0a support for callback in the request token request.
- parameters['oauth_callback'] = callback
-
- return OAuthRequest(http_method, http_url, parameters)
- from_consumer_and_token = staticmethod(from_consumer_and_token)
-
- def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
- http_url=None, parameters=None):
- if not parameters:
- parameters = {}
-
- parameters['oauth_token'] = token.key
-
- if callback:
- parameters['oauth_callback'] = callback
-
- return OAuthRequest(http_method, http_url, parameters)
- from_token_and_callback = staticmethod(from_token_and_callback)
-
- def _split_header(header):
- """Turn Authorization: header into parameters."""
- params = {}
- parts = header.split(',')
- for param in parts:
- # Ignore realm parameter.
- if param.find('realm') > -1:
- continue
- # Remove whitespace.
- param = param.strip()
- # Split key-value.
- param_parts = param.split('=', 1)
- # Remove quotes and unescape the value.
- params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
- return params
- _split_header = staticmethod(_split_header)
-
- def _split_url_string(param_str):
- """Turn URL string into parameters."""
- parameters = cgi.parse_qs(param_str, keep_blank_values=False)
- for k, v in parameters.iteritems():
- parameters[k] = urllib.unquote(v[0])
- return parameters
- _split_url_string = staticmethod(_split_url_string)
-
-class OAuthServer(object):
- """A worker to check the validity of a request against a data store."""
- timestamp_threshold = 300 # In seconds, five minutes.
- version = VERSION
- signature_methods = None
- data_store = None
-
- def __init__(self, data_store=None, signature_methods=None):
- self.data_store = data_store
- self.signature_methods = signature_methods or {}
-
- def set_data_store(self, data_store):
- self.data_store = data_store
-
- def get_data_store(self):
- return self.data_store
-
- def add_signature_method(self, signature_method):
- self.signature_methods[signature_method.get_name()] = signature_method
- return self.signature_methods
-
- def fetch_request_token(self, oauth_request):
- """Processes a request_token request and returns the
- request token on success.
- """
- try:
- # Get the request token for authorization.
- token = self._get_token(oauth_request, 'request')
- except OAuthError:
- # No token required for the initial token request.
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- try:
- callback = self.get_callback(oauth_request)
- except OAuthError:
- callback = None # 1.0, no callback specified.
- self._check_signature(oauth_request, consumer, None)
- # Fetch a new token.
- token = self.data_store.fetch_request_token(consumer, callback)
- return token
-
- def fetch_access_token(self, oauth_request):
- """Processes an access_token request and returns the
- access token on success.
- """
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- try:
- verifier = self._get_verifier(oauth_request)
- except OAuthError:
- verifier = None
- # Get the request token.
- token = self._get_token(oauth_request, 'request')
- self._check_signature(oauth_request, consumer, token)
- new_token = self.data_store.fetch_access_token(consumer, token, verifier)
- return new_token
-
- def verify_request(self, oauth_request):
- """Verifies an api call and checks all the parameters."""
- # -> consumer and token
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- # Get the access token.
- token = self._get_token(oauth_request, 'access')
- self._check_signature(oauth_request, consumer, token)
- parameters = oauth_request.get_nonoauth_parameters()
- return consumer, token, parameters
-
- def authorize_token(self, token, user):
- """Authorize a request token."""
- return self.data_store.authorize_request_token(token, user)
-
- def get_callback(self, oauth_request):
- """Get the callback URL."""
- return oauth_request.get_parameter('oauth_callback')
-
- def build_authenticate_header(self, realm=''):
- """Optional support for the authenticate header."""
- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
- def _get_version(self, oauth_request):
- """Verify the correct version request for this server."""
- try:
- version = oauth_request.get_parameter('oauth_version')
- except:
- version = VERSION
- if version and version != self.version:
- raise OAuthError('OAuth version %s not supported.' % str(version))
- return version
-
- def _get_signature_method(self, oauth_request):
- """Figure out the signature with some defaults."""
- try:
- signature_method = oauth_request.get_parameter(
- 'oauth_signature_method')
- except:
- signature_method = SIGNATURE_METHOD
- try:
- # Get the signature method object.
- signature_method = self.signature_methods[signature_method]
- except:
- signature_method_names = ', '.join(self.signature_methods.keys())
- raise OAuthError('Signature method %s not supported try one of the '
- 'following: %s' % (signature_method, signature_method_names))
-
- return signature_method
-
- def _get_consumer(self, oauth_request):
- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- consumer = self.data_store.lookup_consumer(consumer_key)
- if not consumer:
- raise OAuthError('Invalid consumer.')
- return consumer
-
- def _get_token(self, oauth_request, token_type='access'):
- """Try to find the token for the provided request token key."""
- token_field = oauth_request.get_parameter('oauth_token')
- token = self.data_store.lookup_token(token_type, token_field)
- if not token:
- raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
- return token
-
- def _get_verifier(self, oauth_request):
- return oauth_request.get_parameter('oauth_verifier')
-
- def _check_signature(self, oauth_request, consumer, token):
- timestamp, nonce = oauth_request._get_timestamp_nonce()
- self._check_timestamp(timestamp)
- self._check_nonce(consumer, token, nonce)
- signature_method = self._get_signature_method(oauth_request)
- try:
- signature = oauth_request.get_parameter('oauth_signature')
- except:
- raise OAuthError('Missing signature.')
- # Validate the signature.
- valid_sig = signature_method.check_signature(oauth_request, consumer,
- token, signature)
- if not valid_sig:
- key, base = signature_method.build_signature_base_string(
- oauth_request, consumer, token)
- raise OAuthError('Invalid signature. Expected signature base '
- 'string: %s' % base)
- built = signature_method.build_signature(oauth_request, consumer, token)
-
- def _check_timestamp(self, timestamp):
- """Verify that timestamp is recentish."""
- timestamp = int(timestamp)
- now = int(time.time())
- lapsed = now - timestamp
- if lapsed > self.timestamp_threshold:
- raise OAuthError('Expired timestamp: given %d and now %s has a '
- 'greater difference than threshold %d' %
- (timestamp, now, self.timestamp_threshold))
-
- def _check_nonce(self, consumer, token, nonce):
- """Verify that the nonce is uniqueish."""
- nonce = self.data_store.lookup_nonce(consumer, token, nonce)
- if nonce:
- raise OAuthError('Nonce already used: %s' % str(nonce))
-
-
-class OAuthClient(object):
- """OAuthClient is a worker to attempt to execute a request."""
- consumer = None
- token = None
-
- def __init__(self, oauth_consumer, oauth_token):
- self.consumer = oauth_consumer
- self.token = oauth_token
-
- def get_consumer(self):
- return self.consumer
-
- def get_token(self):
- return self.token
-
- def fetch_request_token(self, oauth_request):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def fetch_access_token(self, oauth_request):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def access_resource(self, oauth_request):
- """-> Some protected resource."""
- raise NotImplementedError
-
-
-class OAuthDataStore(object):
- """A database abstraction used to lookup consumers and tokens."""
-
- def lookup_consumer(self, key):
- """-> OAuthConsumer."""
- raise NotImplementedError
-
- def lookup_token(self, oauth_consumer, token_type, token_token):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def fetch_request_token(self, oauth_consumer, oauth_callback):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def authorize_request_token(self, oauth_token, user):
- """-> OAuthToken."""
- raise NotImplementedError
-
-
-class OAuthSignatureMethod(object):
- """A strategy class that implements a signature method."""
- def get_name(self):
- """-> str."""
- raise NotImplementedError
-
- def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
- """-> str key, str raw."""
- raise NotImplementedError
-
- def build_signature(self, oauth_request, oauth_consumer, oauth_token):
- """-> str."""
- raise NotImplementedError
-
- def check_signature(self, oauth_request, consumer, token, signature):
- built = self.build_signature(oauth_request, consumer, token)
- return built == signature
-
-
-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
-
- def get_name(self):
- return 'HMAC-SHA1'
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- sig = (
- escape(oauth_request.get_normalized_http_method()),
- escape(oauth_request.get_normalized_http_url()),
- escape(oauth_request.get_normalized_parameters()),
- )
-
- key = '%s&' % escape(consumer.secret)
- if token:
- key += escape(token.secret)
- raw = '&'.join(sig)
- return key, raw
-
- def build_signature(self, oauth_request, consumer, token):
- """Builds the base signature string."""
- key, raw = self.build_signature_base_string(oauth_request, consumer,
- token)
-
- # HMAC object.
- try:
- import hashlib # 2.5
- hashed = hmac.new(key, raw, hashlib.sha1)
- except:
- import sha # Deprecated
- hashed = hmac.new(key, raw, sha)
-
- # Calculate the digest base 64.
- return binascii.b2a_base64(hashed.digest())[:-1]
-
-
-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
-
- def get_name(self):
- return 'PLAINTEXT'
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- """Concatenates the consumer key and secret."""
- sig = '%s&' % escape(consumer.secret)
- if token:
- sig = sig + escape(token.secret)
- return sig, sig
-
- def build_signature(self, oauth_request, consumer, token):
- key, raw = self.build_signature_base_string(oauth_request, consumer,
- token)
- return key
\ No newline at end of file
--- /dev/null
+"""
+ oauthlib
+ ~~~~~~~~
+
+ A generic, spec-compliant, thorough implementation of the OAuth
+ request-signing logic.
+
+ :copyright: (c) 2011 by Idan Gazit.
+ :license: BSD, see LICENSE for details.
+"""
+
+__author__ = 'Idan Gazit <idan@gazit.me>'
+__version__ = '0.7.2'
+
+
+import logging
+try: # Python 2.7+
+ from logging import NullHandler
+except ImportError:
+ class NullHandler(logging.Handler):
+
+ def emit(self, record):
+ pass
+
+logging.getLogger('oauthlib').addHandler(NullHandler())
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.common
+~~~~~~~~~~~~~~
+
+This module provides data structures and utilities common
+to all implementations of OAuth.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import collections
+import datetime
+import logging
+import random
+import re
+import sys
+import time
+
+try:
+ from urllib import quote as _quote
+ from urllib import unquote as _unquote
+ from urllib import urlencode as _urlencode
+except ImportError:
+ from urllib.parse import quote as _quote
+ from urllib.parse import unquote as _unquote
+ from urllib.parse import urlencode as _urlencode
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ '0123456789')
+
+CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
+ 'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
+
+
+always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ 'abcdefghijklmnopqrstuvwxyz'
+ '0123456789' '_.-')
+
+log = logging.getLogger('oauthlib')
+
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+ unicode_type = str
+ bytes_type = bytes
+else:
+ unicode_type = unicode
+ bytes_type = str
+
+
+# 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
+def quote(s, safe=b'/'):
+ s = s.encode('utf-8') if isinstance(s, unicode_type) else s
+ s = _quote(s, safe)
+ # PY3 always returns unicode. PY2 may return either, depending on whether
+ # it had to modify the string.
+ if isinstance(s, bytes_type):
+ s = s.decode('utf-8')
+ return s
+
+
+def unquote(s):
+ s = _unquote(s)
+ # PY3 always returns unicode. PY2 seems to always return what you give it,
+ # which differs from quote's behavior. Just to be safe, make sure it is
+ # unicode before we return.
+ if isinstance(s, bytes_type):
+ s = s.decode('utf-8')
+ return s
+
+
+def urlencode(params):
+ utf8_params = encode_params_utf8(params)
+ urlencoded = _urlencode(utf8_params)
+ if isinstance(urlencoded, unicode_type): # PY3 returns unicode
+ return urlencoded
+ else:
+ return urlencoded.decode("utf-8")
+
+
+def encode_params_utf8(params):
+ """Ensures that all parameters in a list of 2-element tuples are encoded to
+ bytestrings using UTF-8
+ """
+ encoded = []
+ for k, v in params:
+ encoded.append((
+ k.encode('utf-8') if isinstance(k, unicode_type) else k,
+ v.encode('utf-8') if isinstance(v, unicode_type) else v))
+ return encoded
+
+
+def decode_params_utf8(params):
+ """Ensures that all parameters in a list of 2-element tuples are decoded to
+ unicode using UTF-8.
+ """
+ decoded = []
+ for k, v in params:
+ decoded.append((
+ k.decode('utf-8') if isinstance(k, bytes_type) else k,
+ v.decode('utf-8') if isinstance(v, bytes_type) else v))
+ return decoded
+
+
+urlencoded = set(always_safe) | set('=&;%+~,*@')
+
+
+def urldecode(query):
+ """Decode a query string in x-www-form-urlencoded format into a sequence
+ of two-element tuples.
+
+ Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
+ correct formatting of the query string by validation. If validation fails
+ a ValueError will be raised. urllib.parse_qsl will only raise errors if
+ any of name-value pairs omits the equals sign.
+ """
+ # Check if query contains invalid characters
+ if query and not set(query) <= urlencoded:
+ error = ("Error trying to decode a non urlencoded string. "
+ "Found invalid characters: %s "
+ "in the string: '%s'. "
+ "Please ensure the request/response body is "
+ "x-www-form-urlencoded.")
+ raise ValueError(error % (set(query) - urlencoded, query))
+
+ # Check for correctly hex encoded values using a regular expression
+ # All encoded values begin with % followed by two hex characters
+ # correct = %00, %A0, %0A, %FF
+ # invalid = %G0, %5H, %PO
+ invalid_hex = '%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]'
+ if len(re.findall(invalid_hex, query)):
+ raise ValueError('Invalid hex encoding in query string.')
+
+ # We encode to utf-8 prior to parsing because parse_qsl behaves
+ # differently on unicode input in python 2 and 3.
+ # Python 2.7
+ # >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
+ # u'\xe5\x95\xa6\xe5\x95\xa6'
+ # Python 2.7, non unicode input gives the same
+ # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
+ # '\xe5\x95\xa6\xe5\x95\xa6'
+ # but now we can decode it to unicode
+ # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
+ # u'\u5566\u5566'
+ # Python 3.3 however
+ # >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
+ # u'\u5566\u5566'
+ query = query.encode(
+ 'utf-8') if not PY3 and isinstance(query, unicode_type) else query
+ # We want to allow queries such as "c2" whereas urlparse.parse_qsl
+ # with the strict_parsing flag will not.
+ params = urlparse.parse_qsl(query, keep_blank_values=True)
+
+ # unicode all the things
+ return decode_params_utf8(params)
+
+
+def extract_params(raw):
+ """Extract parameters and return them as a list of 2-tuples.
+
+ Will successfully extract parameters from urlencoded query strings,
+ dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
+ empty list of parameters. Any other input will result in a return
+ value of None.
+ """
+ if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
+ try:
+ params = urldecode(raw)
+ except ValueError:
+ params = None
+ elif hasattr(raw, '__iter__'):
+ try:
+ dict(raw)
+ except ValueError:
+ params = None
+ except TypeError:
+ params = None
+ else:
+ params = list(raw.items() if isinstance(raw, dict) else raw)
+ params = decode_params_utf8(params)
+ else:
+ params = None
+
+ return params
+
+
+def generate_nonce():
+ """Generate pseudorandom nonce that is unlikely to repeat.
+
+ Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
+ Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+ A random 64-bit number is appended to the epoch timestamp for both
+ randomness and to decrease the likelihood of collisions.
+
+ .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ """
+ return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
+
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC).
+
+ Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
+ Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+ .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ """
+ return unicode_type(int(time.time()))
+
+
+def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
+ """Generates a non-guessable OAuth token
+
+ OAuth (1 and 2) does not specify the format of tokens except that they
+ should be strings of random characters. Tokens should not be guessable
+ and entropy when generating the random characters is important. Which is
+ why SystemRandom is used instead of the default random.choice method.
+ """
+ rand = random.SystemRandom()
+ return ''.join(rand.choice(chars) for x in range(length))
+
+
+def generate_signed_token(private_pem, request):
+ import jwt
+
+ now = datetime.datetime.utcnow()
+
+ claims = {
+ 'scope': request.scope,
+ 'exp': now + datetime.timedelta(seconds=request.expires_in)
+ }
+
+ claims.update(request.claims)
+
+ token = jwt.encode(claims, private_pem, 'RS256')
+ token = to_unicode(token, "UTF-8")
+
+ return token
+
+
+def verify_signed_token(public_pem, token):
+ import jwt
+
+ return jwt.decode(token, public_pem, algorithms=['RS256'])
+
+
+def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
+ """Generates an OAuth client_id
+
+ OAuth 2 specify the format of client_id in
+ http://tools.ietf.org/html/rfc6749#appendix-A.
+ """
+ return generate_token(length, chars)
+
+
+def add_params_to_qs(query, params):
+ """Extend a query with a list of two-tuples."""
+ if isinstance(params, dict):
+ params = params.items()
+ queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
+ queryparams.extend(params)
+ return urlencode(queryparams)
+
+
+def add_params_to_uri(uri, params, fragment=False):
+ """Add a list of two-tuples to the uri query components."""
+ sch, net, path, par, query, fra = urlparse.urlparse(uri)
+ if fragment:
+ fra = add_params_to_qs(fra, params)
+ else:
+ query = add_params_to_qs(query, params)
+ return urlparse.urlunparse((sch, net, path, par, query, fra))
+
+
+def safe_string_equals(a, b):
+ """ Near-constant time string comparison.
+
+ Used in order to avoid timing attacks on sensitive information such
+ as secret keys during request verification (`rootLabs`_).
+
+ .. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
+
+ """
+ if len(a) != len(b):
+ return False
+
+ result = 0
+ for x, y in zip(a, b):
+ result |= ord(x) ^ ord(y)
+ return result == 0
+
+
+def to_unicode(data, encoding='UTF-8'):
+ """Convert a number of different types of objects to unicode."""
+ if isinstance(data, unicode_type):
+ return data
+
+ if isinstance(data, bytes_type):
+ return unicode_type(data, encoding=encoding)
+
+ if hasattr(data, '__iter__'):
+ try:
+ dict(data)
+ except TypeError:
+ pass
+ except ValueError:
+ # Assume it's a one dimensional data structure
+ return (to_unicode(i, encoding) for i in data)
+ else:
+ # We support 2.6 which lacks dict comprehensions
+ if hasattr(data, 'items'):
+ data = data.items()
+ return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
+
+ return data
+
+
+class CaseInsensitiveDict(dict):
+
+ """Basic case insensitive dict with strings only keys."""
+
+ proxy = {}
+
+ def __init__(self, data):
+ self.proxy = dict((k.lower(), k) for k in data)
+ for k in data:
+ self[k] = data[k]
+
+ def __contains__(self, k):
+ return k.lower() in self.proxy
+
+ def __delitem__(self, k):
+ key = self.proxy[k.lower()]
+ super(CaseInsensitiveDict, self).__delitem__(key)
+ del self.proxy[k.lower()]
+
+ def __getitem__(self, k):
+ key = self.proxy[k.lower()]
+ return super(CaseInsensitiveDict, self).__getitem__(key)
+
+ def get(self, k, default=None):
+ return self[k] if k in self else default
+
+ def __setitem__(self, k, v):
+ super(CaseInsensitiveDict, self).__setitem__(k, v)
+ self.proxy[k.lower()] = k
+
+
+class Request(object):
+
+ """A malleable representation of a signable HTTP request.
+
+ Body argument may contain any data, but parameters will only be decoded if
+ they are one of:
+
+ * urlencoded query string
+ * dict
+ * list of 2-tuples
+
+ Anything else will be treated as raw body data to be passed through
+ unmolested.
+ """
+
+ def __init__(self, uri, http_method='GET', body=None, headers=None,
+ encoding='utf-8'):
+ # Convert to unicode using encoding if given, else assume unicode
+ encode = lambda x: to_unicode(x, encoding) if encoding else x
+
+ self.uri = encode(uri)
+ self.http_method = encode(http_method)
+ self.headers = CaseInsensitiveDict(encode(headers or {}))
+ self.body = encode(body)
+ self.decoded_body = extract_params(encode(body))
+ self.oauth_params = []
+
+ self._params = {}
+ self._params.update(dict(urldecode(self.uri_query)))
+ self._params.update(dict(self.decoded_body or []))
+ self._params.update(self.headers)
+
+ def __getattr__(self, name):
+ return self._params.get(name, None)
+
+ def __repr__(self):
+ return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
+ self.uri, self.http_method, self.headers, self.body)
+
+ @property
+ def uri_query(self):
+ return urlparse.urlparse(self.uri).query
+
+ @property
+ def uri_query_params(self):
+ if not self.uri_query:
+ return []
+ return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
+ strict_parsing=True)
+
+ @property
+ def duplicate_params(self):
+ seen_keys = collections.defaultdict(int)
+ all_keys = (p[0]
+ for p in (self.decoded_body or []) + self.uri_query_params)
+ for k in all_keys:
+ seen_keys[k] += 1
+ return [k for k, c in seen_keys.items() if c > 1]
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 1.0 Client
+and Server classes.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .rfc5849 import Client
+from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
+from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
+from .rfc5849 import SIGNATURE_TYPE_BODY
+from .rfc5849.request_validator import RequestValidator
+from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
+from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
+from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
+from .rfc5849.errors import *
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+log = logging.getLogger(__name__)
+
+import sys
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+if sys.version_info[0] == 3:
+ bytes_type = bytes
+else:
+ bytes_type = str
+
+from oauthlib.common import Request, urlencode, generate_nonce
+from oauthlib.common import generate_timestamp, to_unicode
+from . import parameters, signature
+
+SIGNATURE_HMAC = "HMAC-SHA1"
+SIGNATURE_RSA = "RSA-SHA1"
+SIGNATURE_PLAINTEXT = "PLAINTEXT"
+SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
+
+SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
+SIGNATURE_TYPE_QUERY = 'QUERY'
+SIGNATURE_TYPE_BODY = 'BODY'
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+
+
+class Client(object):
+
+ """A client used to sign OAuth 1.0 RFC 5849 requests."""
+ SIGNATURE_METHODS = {
+ SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client,
+ SIGNATURE_RSA: signature.sign_rsa_sha1_with_client,
+ SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
+ }
+
+ @classmethod
+ def register_signature_method(cls, method_name, method_callback):
+ cls.SIGNATURE_METHODS[method_name] = method_callback
+
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None, verifier=None, realm=None,
+ encoding='utf-8', decoding=None,
+ nonce=None, timestamp=None):
+ """Create an OAuth 1 client.
+
+ :param client_key: Client key (consumer key), mandatory.
+ :param resource_owner_key: Resource owner key (oauth token).
+ :param resource_owner_secret: Resource owner secret (oauth token secret).
+ :param callback_uri: Callback used when obtaining request token.
+ :param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
+ :param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
+ SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
+ depending on where you want to embed the oauth
+ credentials.
+ :param rsa_key: RSA key used with SIGNATURE_RSA.
+ :param verifier: Verifier used when obtaining an access token.
+ :param realm: Realm (scope) to which access is being requested.
+ :param encoding: If you provide non-unicode input you may use this
+ to have oauthlib automatically convert.
+ :param decoding: If you wish that the returned uri, headers and body
+ from sign be encoded back from unicode, then set
+ decoding to your preferred encoding, i.e. utf-8.
+ :param nonce: Use this nonce instead of generating one. (Mainly for testing)
+ :param timestamp: Use this timestamp instead of using current. (Mainly for testing)
+ """
+ # Convert to unicode using encoding if given, else assume unicode
+ encode = lambda x: to_unicode(x, encoding) if encoding else x
+
+ self.client_key = encode(client_key)
+ self.client_secret = encode(client_secret)
+ self.resource_owner_key = encode(resource_owner_key)
+ self.resource_owner_secret = encode(resource_owner_secret)
+ self.signature_method = encode(signature_method)
+ self.signature_type = encode(signature_type)
+ self.callback_uri = encode(callback_uri)
+ self.rsa_key = encode(rsa_key)
+ self.verifier = encode(verifier)
+ self.realm = encode(realm)
+ self.encoding = encode(encoding)
+ self.decoding = encode(decoding)
+ self.nonce = encode(nonce)
+ self.timestamp = encode(timestamp)
+
+ if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
+ raise ValueError(
+ 'rsa_key is required when using RSA signature method.')
+
+ def __repr__(self):
+ attrs = vars(self).copy()
+ attrs['client_secret'] = '****' if attrs['client_secret'] else None
+ attrs[
+ 'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
+ attribute_str = ', '.join('%s=%s' % (k, v) for k, v in attrs.items())
+ return '<%s %s>' % (self.__class__.__name__, attribute_str)
+
+ def get_oauth_signature(self, request):
+ """Get an OAuth signature to be used in signing a request
+
+ To satisfy `section 3.4.1.2`_ item 2, if the request argument's
+ headers dict attribute contains a Host item, its value will
+ replace any netloc part of the request argument's uri attribute
+ value.
+
+ .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ """
+ if self.signature_method == SIGNATURE_PLAINTEXT:
+ # fast-path
+ return signature.sign_plaintext(self.client_secret,
+ self.resource_owner_secret)
+
+ uri, headers, body = self._render(request)
+
+ collected_params = signature.collect_parameters(
+ uri_query=urlparse.urlparse(uri).query,
+ body=body,
+ headers=headers)
+ log.debug("Collected params: {0}".format(collected_params))
+
+ normalized_params = signature.normalize_parameters(collected_params)
+ normalized_uri = signature.normalize_base_string_uri(uri,
+ headers.get('Host', None))
+ log.debug("Normalized params: {0}".format(normalized_params))
+ log.debug("Normalized URI: {0}".format(normalized_uri))
+
+ base_string = signature.construct_base_string(request.http_method,
+ normalized_uri, normalized_params)
+
+ log.debug("Base signing string: {0}".format(base_string))
+
+ if self.signature_method not in self.SIGNATURE_METHODS:
+ raise ValueError('Invalid signature method.')
+
+ sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
+
+ log.debug("Signature: {0}".format(sig))
+ return sig
+
+ def get_oauth_params(self, request):
+ """Get the basic OAuth parameters to be used in generating a signature.
+ """
+ nonce = (generate_nonce()
+ if self.nonce is None else self.nonce)
+ timestamp = (generate_timestamp()
+ if self.timestamp is None else self.timestamp)
+ params = [
+ ('oauth_nonce', nonce),
+ ('oauth_timestamp', timestamp),
+ ('oauth_version', '1.0'),
+ ('oauth_signature_method', self.signature_method),
+ ('oauth_consumer_key', self.client_key),
+ ]
+ if self.resource_owner_key:
+ params.append(('oauth_token', self.resource_owner_key))
+ if self.callback_uri:
+ params.append(('oauth_callback', self.callback_uri))
+ if self.verifier:
+ params.append(('oauth_verifier', self.verifier))
+
+ return params
+
+ def _render(self, request, formencode=False, realm=None):
+ """Render a signed request according to signature type
+
+ Returns a 3-tuple containing the request URI, headers, and body.
+
+ If the formencode argument is True and the body contains parameters, it
+ is escaped and returned as a valid formencoded string.
+ """
+ # TODO what if there are body params on a header-type auth?
+ # TODO what if there are query params on a body-type auth?
+
+ uri, headers, body = request.uri, request.headers, request.body
+
+ # TODO: right now these prepare_* methods are very narrow in scope--they
+ # only affect their little thing. In some cases (for example, with
+ # header auth) it might be advantageous to allow these methods to touch
+ # other parts of the request, like the headers—so the prepare_headers
+ # method could also set the Content-Type header to x-www-form-urlencoded
+ # like the spec requires. This would be a fundamental change though, and
+ # I'm not sure how I feel about it.
+ if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
+ headers = parameters.prepare_headers(
+ request.oauth_params, request.headers, realm=realm)
+ elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
+ body = parameters.prepare_form_encoded_body(
+ request.oauth_params, request.decoded_body)
+ if formencode:
+ body = urlencode(body)
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ elif self.signature_type == SIGNATURE_TYPE_QUERY:
+ uri = parameters.prepare_request_uri_query(
+ request.oauth_params, request.uri)
+ else:
+ raise ValueError('Unknown signature type specified.')
+
+ return uri, headers, body
+
+ def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
+ """Sign a request
+
+ Signs an HTTP request with the specified parts.
+
+ Returns a 3-tuple of the signed request's URI, headers, and body.
+ Note that http_method is not returned as it is unaffected by the OAuth
+ signing process. Also worth noting is that duplicate parameters
+ will be included in the signature, regardless of where they are
+ specified (query, body).
+
+ The body argument may be a dict, a list of 2-tuples, or a formencoded
+ string. The Content-Type header must be 'application/x-www-form-urlencoded'
+ if it is present.
+
+ If the body argument is not one of the above, it will be returned
+ verbatim as it is unaffected by the OAuth signing process. Attempting to
+ sign a request with non-formencoded data using the OAuth body signature
+ type is invalid and will raise an exception.
+
+ If the body does contain parameters, it will be returned as a properly-
+ formatted formencoded string.
+
+ Body may not be included if the http_method is either GET or HEAD as
+ this changes the semantic meaning of the request.
+
+ All string data MUST be unicode or be encoded with the same encoding
+ scheme supplied to the Client constructor, default utf-8. This includes
+ strings inside body dicts, for example.
+ """
+ # normalize request data
+ request = Request(uri, http_method, body, headers,
+ encoding=self.encoding)
+
+ # sanity check
+ content_type = request.headers.get('Content-Type', None)
+ multipart = content_type and content_type.startswith('multipart/')
+ should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
+ has_params = request.decoded_body is not None
+ # 3.4.1.3.1. Parameter Sources
+ # [Parameters are collected from the HTTP request entity-body, but only
+ # if [...]:
+ # * The entity-body is single-part.
+ if multipart and has_params:
+ raise ValueError(
+ "Headers indicate a multipart body but body contains parameters.")
+ # * The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # [W3C.REC-html40-19980424].
+ elif should_have_params and not has_params:
+ raise ValueError(
+ "Headers indicate a formencoded body but body was not decodable.")
+ # * The HTTP request entity-header includes the "Content-Type"
+ # header field set to "application/x-www-form-urlencoded".
+ elif not should_have_params and has_params:
+ raise ValueError(
+ "Body contains parameters but Content-Type header was {0} "
+ "instead of {1}".format(content_type or "not set",
+ CONTENT_TYPE_FORM_URLENCODED))
+
+ # 3.5.2. Form-Encoded Body
+ # Protocol parameters can be transmitted in the HTTP request entity-
+ # body, but only if the following REQUIRED conditions are met:
+ # o The entity-body is single-part.
+ # o The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # [W3C.REC-html40-19980424].
+ # o The HTTP request entity-header includes the "Content-Type" header
+ # field set to "application/x-www-form-urlencoded".
+ elif self.signature_type == SIGNATURE_TYPE_BODY and not (
+ should_have_params and has_params and not multipart):
+ raise ValueError(
+ 'Body signatures may only be used with form-urlencoded content')
+
+ # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ # with the clause that parameters from body should only be included
+ # in non GET or HEAD requests. Extracting the request body parameters
+ # and including them in the signature base string would give semantic
+ # meaning to the body, which it should not have according to the
+ # HTTP 1.1 spec.
+ elif http_method.upper() in ('GET', 'HEAD') and has_params:
+ raise ValueError('GET/HEAD requests should not include body.')
+
+ # generate the basic OAuth parameters
+ request.oauth_params = self.get_oauth_params(request)
+
+ # generate the signature
+ request.oauth_params.append(
+ ('oauth_signature', self.get_oauth_signature(request)))
+
+ # render the signed request and return it
+ uri, headers, body = self._render(request, formencode=True,
+ realm=(realm or self.realm))
+
+ if self.decoding:
+ log.debug('Encoding URI, headers and body to %s.', self.decoding)
+ uri = uri.encode(self.decoding)
+ body = body.encode(self.decoding) if body else body
+ new_headers = {}
+ for k, v in headers.items():
+ new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
+ headers = new_headers
+ return uri, headers, body
--- /dev/null
+from __future__ import absolute_import
+
+from .base import BaseEndpoint
+from .request_token import RequestTokenEndpoint
+from .authorization import AuthorizationEndpoint
+from .access_token import AccessTokenEndpoint
+from .resource import ResourceEndpoint
+from .signature_only import SignatureOnlyEndpoint
+from .pre_configured import WebApplicationServer
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.access_token
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the access token provider logic of
+OAuth 1.0 RFC 5849. It validates the correctness of access token requests,
+creates and persists tokens as well as create the proper response to be
+returned to the client.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import urlencode
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class AccessTokenEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for providing OAuth 1 access tokens.
+
+ Typical use is to instantiate with a request validator and invoke the
+ ``create_access_token_response`` from a view function. The tuple returned
+ has all information necessary (body, status, headers) to quickly form
+ and return a proper response. See :doc:`/oauth1/validator` for details on which
+ validator methods to implement for this endpoint.
+ """
+
+ def create_access_token(self, request, credentials):
+ """Create and save a new access token.
+
+ Similar to OAuth 2, indication of granted scopes will be included as a
+ space separated list in ``oauth_authorized_realms``.
+
+ :param request: An oauthlib.common.Request object.
+ :returns: The token as an urlencoded string.
+ """
+ request.realms = self.request_validator.get_realms(
+ request.resource_owner_key, request)
+ token = {
+ 'oauth_token': self.token_generator(),
+ 'oauth_token_secret': self.token_generator(),
+ # Backport the authorized scopes indication used in OAuth2
+ 'oauth_authorized_realms': ' '.join(request.realms)
+ }
+ token.update(credentials)
+ self.request_validator.save_access_token(token, request)
+ return urlencode(token.items())
+
+ def create_access_token_response(self, uri, http_method='GET', body=None,
+ headers=None, credentials=None):
+ """Create an access token response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param credentials: A list of extra credentials to include in the token.
+ :returns: A tuple of 3 elements.
+ 1. A dict of headers to set on the response.
+ 2. The response body as a string.
+ 3. The response status code as an integer.
+
+ An example of a valid request::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import AccessTokenEndpoint
+ >>> endpoint = AccessTokenEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_access_token_response(
+ ... 'https://your.provider/access_token?foo=bar',
+ ... headers={
+ ... 'Authorization': 'OAuth oauth_token=234lsdkf....'
+ ... },
+ ... credentials={
+ ... 'my_specific': 'argument',
+ ... })
+ >>> h
+ {'Content-Type': 'application/x-www-form-urlencoded'}
+ >>> b
+ 'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_authorized_realms=movies+pics&my_specific=argument'
+ >>> s
+ 200
+
+ An response to invalid request would have a different body and status::
+
+ >>> b
+ 'error=invalid_request&description=missing+resource+owner+key'
+ >>> s
+ 400
+
+ The same goes for an an unauthorized request:
+
+ >>> b
+ ''
+ >>> s
+ 401
+ """
+ resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ valid, processed_request = self.validate_access_token_request(
+ request)
+ if valid:
+ token = self.create_access_token(request, credentials or {})
+ self.request_validator.invalidate_request_token(
+ request.client_key,
+ request.resource_owner_key,
+ request)
+ return resp_headers, token, 200
+ else:
+ return {}, None, 401
+ except errors.OAuth1Error as e:
+ return resp_headers, e.urlencoded, e.status_code
+
+ def validate_access_token_request(self, request):
+ """Validate an access token request.
+
+ :param request: An oauthlib.common.Request object.
+ :raises: OAuth1Error if the request is invalid.
+ :returns: A tuple of 2 elements.
+ 1. The validation result (True or False).
+ 2. The request object.
+ """
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+
+ if not request.resource_owner_key:
+ raise errors.InvalidRequestError(
+ description='Missing resource owner.')
+
+ if not self.request_validator.check_request_token(
+ request.resource_owner_key):
+ raise errors.InvalidRequestError(
+ description='Invalid resource owner key format.')
+
+ if not request.verifier:
+ raise errors.InvalidRequestError(
+ description='Missing verifier.')
+
+ if not self.request_validator.check_verifier(request.verifier):
+ raise errors.InvalidRequestError(
+ description='Invalid verifier format.')
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request,
+ request_token=request.resource_owner_key):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid or expired token.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy token is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable resource owner enumeration
+ valid_resource_owner = self.request_validator.validate_request_token(
+ request.client_key, request.resource_owner_key, request)
+ if not valid_resource_owner:
+ request.resource_owner_key = self.request_validator.dummy_request_token
+
+ # The server MUST verify (Section 3.2) the validity of the request,
+ # ensure that the resource owner has authorized the provisioning of
+ # token credentials to the client, and ensure that the temporary
+ # credentials have not expired or been used before. The server MUST
+ # also verify the verification code received from the client.
+ # .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
+ #
+ # Note that early exit would enable resource owner authorization
+ # verifier enumertion.
+ valid_verifier = self.request_validator.validate_verifier(
+ request.client_key,
+ request.resource_owner_key,
+ request.verifier,
+ request)
+
+ valid_signature = self._check_signature(request, is_token_request=True)
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_resource_owner, valid_verifier,
+ valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client:, %s", valid_client)
+ log.info("Valid token:, %s", valid_resource_owner)
+ log.info("Valid verifier:, %s", valid_verifier)
+ log.info("Valid signature:, %s", valid_signature)
+ return v, request
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.authorization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from oauthlib.common import Request, add_params_to_uri
+
+from .base import BaseEndpoint
+from .. import errors
+try:
+ from urllib import urlencode
+except ImportError:
+ from urllib.parse import urlencode
+
+
+class AuthorizationEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for letting authenticated users authorize access
+ to their protected resources to a client.
+
+ Typical use would be to have two views, one for displaying the authorization
+ form and one to process said form on submission.
+
+ The first view will want to utilize ``get_realms_and_credentials`` to fetch
+ requested realms and useful client credentials, such as name and
+ description, to be used when creating the authorization form.
+
+ During form processing you can use ``create_authorization_response`` to
+ validate the request, create a verifier as well as prepare the final
+ redirection URI used to send the user back to the client.
+
+ See :doc:`/oauth1/validator` for details on which validator methods to implement
+ for this endpoint.
+ """
+
+ def create_verifier(self, request, credentials):
+ """Create and save a new request token.
+
+ :param request: An oauthlib.common.Request object.
+ :param credentials: A dict of extra token credentials.
+ :returns: The verifier as a dict.
+ """
+ verifier = {
+ 'oauth_token': request.resource_owner_key,
+ 'oauth_verifier': self.token_generator(),
+ }
+ verifier.update(credentials)
+ self.request_validator.save_verifier(
+ request.resource_owner_key, verifier, request)
+ return verifier
+
+ def create_authorization_response(self, uri, http_method='GET', body=None,
+ headers=None, realms=None, credentials=None):
+ """Create an authorization response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param credentials: A list of credentials to include in the verifier.
+ :returns: A tuple of 3 elements.
+ 1. A dict of headers to set on the response.
+ 2. The response body as a string.
+ 3. The response status code as an integer.
+
+ If the callback URI tied to the current token is "oob", a response with
+ a 200 status code will be returned. In this case, it may be desirable to
+ modify the response to better display the verifier to the client.
+
+ An example of an authorization request::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import AuthorizationEndpoint
+ >>> endpoint = AuthorizationEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_authorization_response(
+ ... 'https://your.provider/authorize?oauth_token=...',
+ ... credentials={
+ ... 'extra': 'argument',
+ ... })
+ >>> h
+ {'Location': 'https://the.client/callback?oauth_verifier=...&extra=argument'}
+ >>> b
+ None
+ >>> s
+ 302
+
+ An example of a request with an "oob" callback::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import AuthorizationEndpoint
+ >>> endpoint = AuthorizationEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_authorization_response(
+ ... 'https://your.provider/authorize?foo=bar',
+ ... credentials={
+ ... 'extra': 'argument',
+ ... })
+ >>> h
+ {'Content-Type': 'application/x-www-form-urlencoded'}
+ >>> b
+ 'oauth_verifier=...&extra=argument'
+ >>> s
+ 200
+ """
+ request = self._create_request(uri, http_method=http_method, body=body,
+ headers=headers)
+
+ if not request.resource_owner_key:
+ raise errors.InvalidRequestError(
+ 'Missing mandatory parameter oauth_token.')
+ if not self.request_validator.verify_request_token(
+ request.resource_owner_key, request):
+ raise errors.InvalidClientError()
+
+ request.realms = realms
+ if (request.realms and not self.request_validator.verify_realms(
+ request.resource_owner_key, request.realms, request)):
+ raise errors.InvalidRequestError(
+ description=('User granted access to realms outside of '
+ 'what the client may request.'))
+
+ verifier = self.create_verifier(request, credentials or {})
+ redirect_uri = self.request_validator.get_redirect_uri(
+ request.resource_owner_key, request)
+ if redirect_uri == 'oob':
+ response_headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'}
+ response_body = urlencode(verifier)
+ return response_headers, response_body, 200
+ else:
+ populated_redirect = add_params_to_uri(
+ redirect_uri, verifier.items())
+ return {'Location': populated_redirect}, None, 302
+
+ def get_realms_and_credentials(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Fetch realms and credentials for the presented request token.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :returns: A tuple of 2 elements.
+ 1. A list of request realms.
+ 2. A dict of credentials which may be useful in creating the
+ authorization form.
+ """
+ request = self._create_request(uri, http_method=http_method, body=body,
+ headers=headers)
+
+ if not self.request_validator.verify_request_token(
+ request.resource_owner_key, request):
+ raise errors.InvalidClientError()
+
+ realms = self.request_validator.get_realms(
+ request.resource_owner_key, request)
+ return realms, {'resource_owner_key': request.resource_owner_key}
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.base
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import time
+
+from oauthlib.common import Request, generate_token
+
+from .. import signature, utils, errors
+from .. import CONTENT_TYPE_FORM_URLENCODED
+from .. import SIGNATURE_HMAC, SIGNATURE_RSA
+from .. import SIGNATURE_TYPE_AUTH_HEADER
+from .. import SIGNATURE_TYPE_QUERY
+from .. import SIGNATURE_TYPE_BODY
+
+
+class BaseEndpoint(object):
+
+ def __init__(self, request_validator, token_generator=None):
+ self.request_validator = request_validator
+ self.token_generator = token_generator or generate_token
+
+ def _get_signature_type_and_params(self, request):
+ """Extracts parameters from query, headers and body. Signature type
+ is set to the source in which parameters were found.
+ """
+ # Per RFC5849, only the Authorization header may contain the 'realm'
+ # optional parameter.
+ header_params = signature.collect_parameters(headers=request.headers,
+ exclude_oauth_signature=False, with_realm=True)
+ body_params = signature.collect_parameters(body=request.body,
+ exclude_oauth_signature=False)
+ query_params = signature.collect_parameters(uri_query=request.uri_query,
+ exclude_oauth_signature=False)
+
+ params = []
+ params.extend(header_params)
+ params.extend(body_params)
+ params.extend(query_params)
+ signature_types_with_oauth_params = list(filter(lambda s: s[2], (
+ (SIGNATURE_TYPE_AUTH_HEADER, params,
+ utils.filter_oauth_params(header_params)),
+ (SIGNATURE_TYPE_BODY, params,
+ utils.filter_oauth_params(body_params)),
+ (SIGNATURE_TYPE_QUERY, params,
+ utils.filter_oauth_params(query_params))
+ )))
+
+ if len(signature_types_with_oauth_params) > 1:
+ found_types = [s[0] for s in signature_types_with_oauth_params]
+ raise errors.InvalidRequestError(
+ description=('oauth_ params must come from only 1 signature'
+ 'type but were found in %s',
+ ', '.join(found_types)))
+
+ try:
+ signature_type, params, oauth_params = signature_types_with_oauth_params[
+ 0]
+ except IndexError:
+ raise errors.InvalidRequestError(
+ description='Missing mandatory OAuth parameters.')
+
+ return signature_type, params, oauth_params
+
+ def _create_request(self, uri, http_method, body, headers):
+ # Only include body data from x-www-form-urlencoded requests
+ headers = headers or {}
+ if ("Content-Type" in headers and
+ CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
+ request = Request(uri, http_method, body, headers)
+ else:
+ request = Request(uri, http_method, '', headers)
+
+ signature_type, params, oauth_params = (
+ self._get_signature_type_and_params(request))
+
+ # The server SHOULD return a 400 (Bad Request) status code when
+ # receiving a request with duplicated protocol parameters.
+ if len(dict(oauth_params)) != len(oauth_params):
+ raise errors.InvalidRequestError(
+ description='Duplicate OAuth2 entries.')
+
+ oauth_params = dict(oauth_params)
+ request.signature = oauth_params.get('oauth_signature')
+ request.client_key = oauth_params.get('oauth_consumer_key')
+ request.resource_owner_key = oauth_params.get('oauth_token')
+ request.nonce = oauth_params.get('oauth_nonce')
+ request.timestamp = oauth_params.get('oauth_timestamp')
+ request.redirect_uri = oauth_params.get('oauth_callback')
+ request.verifier = oauth_params.get('oauth_verifier')
+ request.signature_method = oauth_params.get('oauth_signature_method')
+ request.realm = dict(params).get('realm')
+ request.oauth_params = oauth_params
+
+ # Parameters to Client depend on signature method which may vary
+ # for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters
+ request.params = [(k, v) for k, v in params if k != "oauth_signature"]
+
+ if 'realm' in request.headers.get('Authorization', ''):
+ request.params = [(k, v)
+ for k, v in request.params if k != "realm"]
+
+ return request
+
+ def _check_transport_security(self, request):
+ # TODO: move into oauthlib.common from oauth2.utils
+ if (self.request_validator.enforce_ssl and
+ not request.uri.lower().startswith("https://")):
+ raise errors.InsecureTransportError()
+
+ def _check_mandatory_parameters(self, request):
+ # The server SHOULD return a 400 (Bad Request) status code when
+ # receiving a request with missing parameters.
+ if not all((request.signature, request.client_key,
+ request.nonce, request.timestamp,
+ request.signature_method)):
+ raise errors.InvalidRequestError(
+ description='Missing mandatory OAuth parameters.')
+
+ # OAuth does not mandate a particular signature method, as each
+ # implementation can have its own unique requirements. Servers are
+ # free to implement and document their own custom methods.
+ # Recommending any particular method is beyond the scope of this
+ # specification. Implementers should review the Security
+ # Considerations section (`Section 4`_) before deciding on which
+ # method to support.
+ # .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4
+ if (not request.signature_method in
+ self.request_validator.allowed_signature_methods):
+ raise errors.InvalidSignatureMethodError(
+ description="Invalid signature, %s not in %r." % (
+ request.signature_method,
+ self.request_validator.allowed_signature_methods))
+
+ # Servers receiving an authenticated request MUST validate it by:
+ # If the "oauth_version" parameter is present, ensuring its value is
+ # "1.0".
+ if ('oauth_version' in request.oauth_params and
+ request.oauth_params['oauth_version'] != '1.0'):
+ raise errors.InvalidRequestError(
+ description='Invalid OAuth version.')
+
+ # The timestamp value MUST be a positive integer. Unless otherwise
+ # specified by the server's documentation, the timestamp is expressed
+ # in the number of seconds since January 1, 1970 00:00:00 GMT.
+ if len(request.timestamp) != 10:
+ raise errors.InvalidRequestError(
+ description='Invalid timestamp size')
+
+ try:
+ ts = int(request.timestamp)
+
+ except ValueError:
+ raise errors.InvalidRequestError(
+ description='Timestamp must be an integer.')
+
+ else:
+ # To avoid the need to retain an infinite number of nonce values for
+ # future checks, servers MAY choose to restrict the time period after
+ # which a request with an old timestamp is rejected.
+ if abs(time.time() - ts) > self.request_validator.timestamp_lifetime:
+ raise errors.InvalidRequestError(
+ description=('Timestamp given is invalid, differ from '
+ 'allowed by over %s seconds.' % (
+ self.request_validator.timestamp_lifetime)))
+
+ # Provider specific validation of parameters, used to enforce
+ # restrictions such as character set and length.
+ if not self.request_validator.check_client_key(request.client_key):
+ raise errors.InvalidRequestError(
+ description='Invalid client key format.')
+
+ if not self.request_validator.check_nonce(request.nonce):
+ raise errors.InvalidRequestError(
+ description='Invalid nonce format.')
+
+ def _check_signature(self, request, is_token_request=False):
+ # ---- RSA Signature verification ----
+ if request.signature_method == SIGNATURE_RSA:
+ # The server verifies the signature per `[RFC3447] section 8.2.2`_
+ # .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1
+ rsa_key = self.request_validator.get_rsa_key(
+ request.client_key, request)
+ valid_signature = signature.verify_rsa_sha1(request, rsa_key)
+
+ # ---- HMAC or Plaintext Signature verification ----
+ else:
+ # Servers receiving an authenticated request MUST validate it by:
+ # Recalculating the request signature independently as described in
+ # `Section 3.4`_ and comparing it to the value received from the
+ # client via the "oauth_signature" parameter.
+ # .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+ client_secret = self.request_validator.get_client_secret(
+ request.client_key, request)
+ resource_owner_secret = None
+ if request.resource_owner_key:
+ if is_token_request:
+ resource_owner_secret = self.request_validator.get_request_token_secret(
+ request.client_key, request.resource_owner_key, request)
+ else:
+ resource_owner_secret = self.request_validator.get_access_token_secret(
+ request.client_key, request.resource_owner_key, request)
+
+ if request.signature_method == SIGNATURE_HMAC:
+ valid_signature = signature.verify_hmac_sha1(request,
+ client_secret, resource_owner_secret)
+ else:
+ valid_signature = signature.verify_plaintext(request,
+ client_secret, resource_owner_secret)
+ return valid_signature
--- /dev/null
+from __future__ import absolute_import, unicode_literals
+
+from . import RequestTokenEndpoint, AuthorizationEndpoint
+from . import AccessTokenEndpoint, ResourceEndpoint
+
+
+class WebApplicationServer(RequestTokenEndpoint, AuthorizationEndpoint,
+ AccessTokenEndpoint, ResourceEndpoint):
+
+ def __init__(self, request_validator):
+ RequestTokenEndpoint.__init__(self, request_validator)
+ AuthorizationEndpoint.__init__(self, request_validator)
+ AccessTokenEndpoint.__init__(self, request_validator)
+ ResourceEndpoint.__init__(self, request_validator)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.request_token
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the request token provider logic of
+OAuth 1.0 RFC 5849. It validates the correctness of request token requests,
+creates and persists tokens as well as create the proper response to be
+returned to the client.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import urlencode
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class RequestTokenEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for providing OAuth 1 request tokens.
+
+ Typical use is to instantiate with a request validator and invoke the
+ ``create_request_token_response`` from a view function. The tuple returned
+ has all information necessary (body, status, headers) to quickly form
+ and return a proper response. See :doc:`/oauth1/validator` for details on which
+ validator methods to implement for this endpoint.
+ """
+
+ def create_request_token(self, request, credentials):
+ """Create and save a new request token.
+
+ :param request: An oauthlib.common.Request object.
+ :param credentials: A dict of extra token credentials.
+ :returns: The token as an urlencoded string.
+ """
+ token = {
+ 'oauth_token': self.token_generator(),
+ 'oauth_token_secret': self.token_generator(),
+ 'oauth_callback_confirmed': 'true'
+ }
+ token.update(credentials)
+ self.request_validator.save_request_token(token, request)
+ return urlencode(token.items())
+
+ def create_request_token_response(self, uri, http_method='GET', body=None,
+ headers=None, credentials=None):
+ """Create a request token response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param credentials: A list of extra credentials to include in the token.
+ :returns: A tuple of 3 elements.
+ 1. A dict of headers to set on the response.
+ 2. The response body as a string.
+ 3. The response status code as an integer.
+
+ An example of a valid request::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import RequestTokenEndpoint
+ >>> endpoint = RequestTokenEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_request_token_response(
+ ... 'https://your.provider/request_token?foo=bar',
+ ... headers={
+ ... 'Authorization': 'OAuth realm=movies user, oauth_....'
+ ... },
+ ... credentials={
+ ... 'my_specific': 'argument',
+ ... })
+ >>> h
+ {'Content-Type': 'application/x-www-form-urlencoded'}
+ >>> b
+ 'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_callback_confirmed=true&my_specific=argument'
+ >>> s
+ 200
+
+ An response to invalid request would have a different body and status::
+
+ >>> b
+ 'error=invalid_request&description=missing+callback+uri'
+ >>> s
+ 400
+
+ The same goes for an an unauthorized request:
+
+ >>> b
+ ''
+ >>> s
+ 401
+ """
+ resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ valid, processed_request = self.validate_request_token_request(
+ request)
+ if valid:
+ token = self.create_request_token(request, credentials or {})
+ return resp_headers, token, 200
+ else:
+ return {}, None, 401
+ except errors.OAuth1Error as e:
+ return resp_headers, e.urlencoded, e.status_code
+
+ def validate_request_token_request(self, request):
+ """Validate a request token request.
+
+ :param request: An oauthlib.common.Request object.
+ :raises: OAuth1Error if the request is invalid.
+ :returns: A tuple of 2 elements.
+ 1. The validation result (True or False).
+ 2. The request object.
+ """
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+
+ if request.realm:
+ request.realms = request.realm.split(' ')
+ else:
+ request.realms = self.request_validator.get_default_realms(
+ request.client_key, request)
+ if not self.request_validator.check_realms(request.realms):
+ raise errors.InvalidRequestError(
+ description='Invalid realm %s. Allowed are %r.' % (
+ request.realms, self.request_validator.realms))
+
+ if not request.redirect_uri:
+ raise errors.InvalidRequestError(
+ description='Missing callback URI.')
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request,
+ request_token=request.resource_owner_key):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ # Note that `realm`_ is only used in authorization headers and how
+ # it should be interepreted is not included in the OAuth spec.
+ # However they could be seen as a scope or realm to which the
+ # client has access and as such every client should be checked
+ # to ensure it is authorized access to that scope or realm.
+ # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
+ #
+ # Note that early exit would enable client realm access enumeration.
+ #
+ # The require_realm indicates this is the first step in the OAuth
+ # workflow where a client requests access to a specific realm.
+ # This first step (obtaining request token) need not require a realm
+ # and can then be identified by checking the require_resource_owner
+ # flag and abscence of realm.
+ #
+ # Clients obtaining an access token will not supply a realm and it will
+ # not be checked. Instead the previously requested realm should be
+ # transferred from the request token to the access token.
+ #
+ # Access to protected resources will always validate the realm but note
+ # that the realm is now tied to the access token and not provided by
+ # the client.
+ valid_realm = self.request_validator.validate_requested_realms(
+ request.client_key, request.realms, request)
+
+ # Callback is normally never required, except for requests for
+ # a Temporary Credential as described in `Section 2.1`_
+ # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1
+ valid_redirect = self.request_validator.validate_redirect_uri(
+ request.client_key, request.redirect_uri, request)
+ if not request.redirect_uri:
+ raise NotImplementedError('Redirect URI must either be provided '
+ 'or set to a default during validation.')
+
+ valid_signature = self._check_signature(request)
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_realm, valid_redirect, valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client: %s.", valid_client)
+ log.info("Valid realm: %s.", valid_realm)
+ log.info("Valid callback: %s.", valid_redirect)
+ log.info("Valid signature: %s.", valid_signature)
+ return v, request
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.resource
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the resource protection provider logic of
+OAuth 1.0 RFC 5849.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class ResourceEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for protecting resources.
+
+ Typical use is to instantiate with a request validator and invoke the
+ ``validate_protected_resource_request`` in a decorator around a view
+ function. If the request is valid, invoke and return the response of the
+ view. If invalid create and return an error response directly from the
+ decorator.
+
+ See :doc:`/oauth1/validator` for details on which validator methods to implement
+ for this endpoint.
+
+ An example decorator::
+
+ from functools import wraps
+ from your_validator import your_validator
+ from oauthlib.oauth1 import ResourceEndpoint
+ endpoint = ResourceEndpoint(your_validator)
+
+ def require_oauth(realms=None):
+ def decorator(f):
+ @wraps(f)
+ def wrapper(request, *args, **kwargs):
+ v, r = provider.validate_protected_resource_request(
+ request.url,
+ http_method=request.method,
+ body=request.data,
+ headers=request.headers,
+ realms=realms or [])
+ if v:
+ return f(*args, **kwargs)
+ else:
+ return abort(403)
+ """
+
+ def validate_protected_resource_request(self, uri, http_method='GET',
+ body=None, headers=None, realms=None):
+ """Create a request token response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param realms: A list of realms the resource is protected under.
+ This will be supplied to the ``validate_realms``
+ method of the request validator.
+ :returns: A tuple of 2 elements.
+ 1. True if valid, False otherwise.
+ 2. An oauthlib.common.Request object.
+ """
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ except errors.OAuth1Error:
+ return False, None
+
+ try:
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+ except errors.OAuth1Error:
+ return False, request
+
+ if not request.resource_owner_key:
+ return False, request
+
+ if not self.request_validator.check_access_token(
+ request.resource_owner_key):
+ return False, request
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request,
+ access_token=request.resource_owner_key):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid or expired token.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy token is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable resource owner enumeration
+ valid_resource_owner = self.request_validator.validate_access_token(
+ request.client_key, request.resource_owner_key, request)
+ if not valid_resource_owner:
+ request.resource_owner_key = self.request_validator.dummy_access_token
+
+ # Note that `realm`_ is only used in authorization headers and how
+ # it should be interepreted is not included in the OAuth spec.
+ # However they could be seen as a scope or realm to which the
+ # client has access and as such every client should be checked
+ # to ensure it is authorized access to that scope or realm.
+ # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
+ #
+ # Note that early exit would enable client realm access enumeration.
+ #
+ # The require_realm indicates this is the first step in the OAuth
+ # workflow where a client requests access to a specific realm.
+ # This first step (obtaining request token) need not require a realm
+ # and can then be identified by checking the require_resource_owner
+ # flag and abscence of realm.
+ #
+ # Clients obtaining an access token will not supply a realm and it will
+ # not be checked. Instead the previously requested realm should be
+ # transferred from the request token to the access token.
+ #
+ # Access to protected resources will always validate the realm but note
+ # that the realm is now tied to the access token and not provided by
+ # the client.
+ valid_realm = self.request_validator.validate_realms(request.client_key,
+ request.resource_owner_key, request, uri=request.uri,
+ realms=realms)
+
+ valid_signature = self._check_signature(request)
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_resource_owner, valid_realm,
+ valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client: %s", valid_client)
+ log.info("Valid token: %s", valid_resource_owner)
+ log.info("Valid realm: %s", valid_realm)
+ log.info("Valid signature: %s", valid_signature)
+ return v, request
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.signature_only
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the signing logic of OAuth 1.0 RFC 5849.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class SignatureOnlyEndpoint(BaseEndpoint):
+
+ """An endpoint only responsible for verifying an oauth signature."""
+
+ def validate_request(self, uri, http_method='GET',
+ body=None, headers=None):
+ """Validate a signed OAuth request.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :returns: A tuple of 2 elements.
+ 1. True if valid, False otherwise.
+ 2. An oauthlib.common.Request object.
+ """
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ except errors.OAuth1Error:
+ return False, None
+
+ try:
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+ except errors.OAuth1Error:
+ return False, request
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ valid_signature = self._check_signature(request)
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client: %s", valid_client)
+ log.info("Valid signature: %s", valid_signature)
+ return v, request
--- /dev/null
+# coding=utf-8
+"""
+oauthlib.oauth1.rfc5849.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 1 clients and provicers to represent the spec
+defined error responses for all four core grant types.
+"""
+from __future__ import unicode_literals
+
+from oauthlib.common import urlencode, add_params_to_uri
+
+
+class OAuth1Error(Exception):
+ error = None
+ description = ''
+
+ def __init__(self, description=None, uri=None, status_code=400,
+ request=None):
+ """
+ description: A human-readable ASCII [USASCII] text providing
+ additional information, used to assist the client
+ developer in understanding the error that occurred.
+ Values for the "error_description" parameter MUST NOT
+ include characters outside the set
+ x20-21 / x23-5B / x5D-7E.
+
+ uri: A URI identifying a human-readable web page with information
+ about the error, used to provide the client developer with
+ additional information about the error. Values for the
+ "error_uri" parameter MUST conform to the URI- Reference
+ syntax, and thus MUST NOT include characters outside the set
+ x21 / x23-5B / x5D-7E.
+
+ state: A CSRF protection value received from the client.
+
+ request: Oauthlib Request object
+ """
+ self.description = description or self.description
+ message = '(%s) %s' % (self.error, self.description)
+ if request:
+ message += ' ' + repr(request)
+ super(OAuth1Error, self).__init__(message)
+
+ self.uri = uri
+ self.status_code = status_code
+
+ def in_uri(self, uri):
+ return add_params_to_uri(uri, self.twotuples)
+
+ @property
+ def twotuples(self):
+ error = [('error', self.error)]
+ if self.description:
+ error.append(('error_description', self.description))
+ if self.uri:
+ error.append(('error_uri', self.uri))
+ return error
+
+ @property
+ def urlencoded(self):
+ return urlencode(self.twotuples)
+
+
+class InsecureTransportError(OAuth1Error):
+ error = 'insecure_transport_protocol'
+ description = 'Only HTTPS connections are permitted.'
+
+
+class InvalidSignatureMethodError(OAuth1Error):
+ error = 'invalid_signature_method'
+
+
+class InvalidRequestError(OAuth1Error):
+ error = 'invalid_request'
+
+
+class InvalidClientError(OAuth1Error):
+ error = 'invalid_client'
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.parameters
+~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
+
+.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5
+"""
+from __future__ import absolute_import, unicode_literals
+
+try:
+ from urlparse import urlparse, urlunparse
+except ImportError:
+ from urllib.parse import urlparse, urlunparse
+from . import utils
+from oauthlib.common import extract_params, urlencode
+
+
+# TODO: do we need filter_params now that oauth_params are handled by Request?
+# We can easily pass in just oauth protocol params.
+@utils.filter_params
+def prepare_headers(oauth_params, headers=None, realm=None):
+ """**Prepare the Authorization header.**
+ Per `section 3.5.1`_ of the spec.
+
+ Protocol parameters can be transmitted using the HTTP "Authorization"
+ header field as defined by `RFC2617`_ with the auth-scheme name set to
+ "OAuth" (case insensitive).
+
+ For example::
+
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="0685bd9184jfhq22",
+ oauth_token="ad180jjd733klru7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+ oauth_timestamp="137131200",
+ oauth_nonce="4572616e48616d6d65724c61686176",
+ oauth_version="1.0"
+
+
+ .. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+ .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+ """
+ headers = headers or {}
+
+ # Protocol parameters SHALL be included in the "Authorization" header
+ # field as follows:
+ authorization_header_parameters_parts = []
+ for oauth_parameter_name, value in oauth_params:
+ # 1. Parameter names and values are encoded per Parameter Encoding
+ # (`Section 3.6`_)
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ escaped_name = utils.escape(oauth_parameter_name)
+ escaped_value = utils.escape(value)
+
+ # 2. Each parameter's name is immediately followed by an "=" character
+ # (ASCII code 61), a """ character (ASCII code 34), the parameter
+ # value (MAY be empty), and another """ character (ASCII code 34).
+ part = '{0}="{1}"'.format(escaped_name, escaped_value)
+
+ authorization_header_parameters_parts.append(part)
+
+ # 3. Parameters are separated by a "," character (ASCII code 44) and
+ # OPTIONAL linear whitespace per `RFC2617`_.
+ #
+ # .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+ authorization_header_parameters = ', '.join(
+ authorization_header_parameters_parts)
+
+ # 4. The OPTIONAL "realm" parameter MAY be added and interpreted per
+ # `RFC2617 section 1.2`_.
+ #
+ # .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2
+ if realm:
+ # NOTE: realm should *not* be escaped
+ authorization_header_parameters = ('realm="%s", ' % realm +
+ authorization_header_parameters)
+
+ # the auth-scheme name set to "OAuth" (case insensitive).
+ authorization_header = 'OAuth %s' % authorization_header_parameters
+
+ # contribute the Authorization header to the given headers
+ full_headers = {}
+ full_headers.update(headers)
+ full_headers['Authorization'] = authorization_header
+ return full_headers
+
+
+def _append_params(oauth_params, params):
+ """Append OAuth params to an existing set of parameters.
+
+ Both params and oauth_params is must be lists of 2-tuples.
+
+ Per `section 3.5.2`_ and `3.5.3`_ of the spec.
+
+ .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+ .. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+
+ """
+ merged = list(params)
+ merged.extend(oauth_params)
+ # The request URI / entity-body MAY include other request-specific
+ # parameters, in which case, the protocol parameters SHOULD be appended
+ # following the request-specific parameters, properly separated by an "&"
+ # character (ASCII code 38)
+ merged.sort(key=lambda i: i[0].startswith('oauth_'))
+ return merged
+
+
+def prepare_form_encoded_body(oauth_params, body):
+ """Prepare the Form-Encoded Body.
+
+ Per `section 3.5.2`_ of the spec.
+
+ .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+
+ """
+ # append OAuth params to the existing body
+ return _append_params(oauth_params, body)
+
+
+def prepare_request_uri_query(oauth_params, uri):
+ """Prepare the Request URI Query.
+
+ Per `section 3.5.3`_ of the spec.
+
+ .. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+
+ """
+ # append OAuth params to the existing set of query components
+ sch, net, path, par, query, fra = urlparse(uri)
+ query = urlencode(
+ _append_params(oauth_params, extract_params(query) or []))
+ return urlunparse((sch, net, path, par, query, fra))
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from . import SIGNATURE_METHODS, utils
+
+
+class RequestValidator(object):
+
+ """A validator/datastore interaction base class for OAuth 1 providers.
+
+ OAuth providers should inherit from RequestValidator and implement the
+ methods and properties outlined below. Further details are provided in the
+ documentation for each method and property.
+
+ Methods used to check the format of input parameters. Common tests include
+ length, character set, membership, range or pattern. These tests are
+ referred to as `whitelisting or blacklisting`_. Whitelisting is better
+ but blacklisting can be usefull to spot malicious activity.
+ The following have methods a default implementation:
+
+ - check_client_key
+ - check_request_token
+ - check_access_token
+ - check_nonce
+ - check_verifier
+ - check_realms
+
+ The methods above default to whitelist input parameters, checking that they
+ are alphanumerical and between a minimum and maximum length. Rather than
+ overloading the methods a few properties can be used to configure these
+ methods.
+
+ * @safe_characters -> (character set)
+ * @client_key_length -> (min, max)
+ * @request_token_length -> (min, max)
+ * @access_token_length -> (min, max)
+ * @nonce_length -> (min, max)
+ * @verifier_length -> (min, max)
+ * @realms -> [list, of, realms]
+
+ Methods used to validate/invalidate input parameters. These checks usually
+ hit either persistent or temporary storage such as databases or the
+ filesystem. See each methods documentation for detailed usage.
+ The following methods must be implemented:
+
+ - validate_client_key
+ - validate_request_token
+ - validate_access_token
+ - validate_timestamp_and_nonce
+ - validate_redirect_uri
+ - validate_requested_realms
+ - validate_realms
+ - validate_verifier
+ - invalidate_request_token
+
+ Methods used to retrieve sensitive information from storage.
+ The following methods must be implemented:
+
+ - get_client_secret
+ - get_request_token_secret
+ - get_access_token_secret
+ - get_rsa_key
+ - get_realms
+ - get_default_realms
+ - get_redirect_uri
+
+ Methods used to save credentials.
+ The following methods must be implemented:
+
+ - save_request_token
+ - save_verifier
+ - save_access_token
+
+ Methods used to verify input parameters. This methods are used during
+ authorizing request token by user (AuthorizationEndpoint), to check if
+ parameters are valid. During token authorization request is not signed,
+ thus 'validation' methods can not be used. The following methods must be
+ implemented:
+
+ - verify_realms
+ - verify_request_token
+
+ To prevent timing attacks it is necessary to not exit early even if the
+ client key or resource owner key is invalid. Instead dummy values should
+ be used during the remaining verification process. It is very important
+ that the dummy client and token are valid input parameters to the methods
+ get_client_secret, get_rsa_key and get_(access/request)_token_secret and
+ that the running time of those methods when given a dummy value remain
+ equivalent to the running time when given a valid client/resource owner.
+ The following properties must be implemented:
+
+ * @dummy_client
+ * @dummy_request_token
+ * @dummy_access_token
+
+ Example implementations have been provided, note that the database used is
+ a simple dictionary and serves only an illustrative purpose. Use whichever
+ database suits your project and how to access it is entirely up to you.
+ The methods are introduced in an order which should make understanding
+ their use more straightforward and as such it could be worth reading what
+ follows in chronological order.
+
+ .. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
+ """
+
+ def __init__(self):
+ pass
+
+ @property
+ def allowed_signature_methods(self):
+ return SIGNATURE_METHODS
+
+ @property
+ def safe_characters(self):
+ return set(utils.UNICODE_ASCII_CHARACTER_SET)
+
+ @property
+ def client_key_length(self):
+ return 20, 30
+
+ @property
+ def request_token_length(self):
+ return 20, 30
+
+ @property
+ def access_token_length(self):
+ return 20, 30
+
+ @property
+ def timestamp_lifetime(self):
+ return 600
+
+ @property
+ def nonce_length(self):
+ return 20, 30
+
+ @property
+ def verifier_length(self):
+ return 20, 30
+
+ @property
+ def realms(self):
+ return []
+
+ @property
+ def enforce_ssl(self):
+ return True
+
+ def check_client_key(self, client_key):
+ """Check that the client key only contains safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.client_key_length
+ return (set(client_key) <= self.safe_characters and
+ lower <= len(client_key) <= upper)
+
+ def check_request_token(self, request_token):
+ """Checks that the request token contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.request_token_length
+ return (set(request_token) <= self.safe_characters and
+ lower <= len(request_token) <= upper)
+
+ def check_access_token(self, request_token):
+ """Checks that the token contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.access_token_length
+ return (set(request_token) <= self.safe_characters and
+ lower <= len(request_token) <= upper)
+
+ def check_nonce(self, nonce):
+ """Checks that the nonce only contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.nonce_length
+ return (set(nonce) <= self.safe_characters and
+ lower <= len(nonce) <= upper)
+
+ def check_verifier(self, verifier):
+ """Checks that the verifier contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.verifier_length
+ return (set(verifier) <= self.safe_characters and
+ lower <= len(verifier) <= upper)
+
+ def check_realms(self, realms):
+ """Check that the realm is one of a set allowed realms."""
+ return all((r in self.realms for r in realms))
+
+ @property
+ def dummy_client(self):
+ """Dummy client used when an invalid client key is supplied.
+
+ :returns: The dummy client key string.
+
+ The dummy client should be associated with either a client secret,
+ a rsa key or both depending on which signature methods are supported.
+ Providers should make sure that
+
+ get_client_secret(dummy_client)
+ get_rsa_key(dummy_client)
+
+ return a valid secret or key for the dummy client.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ @property
+ def dummy_request_token(self):
+ """Dummy request token used when an invalid token was supplied.
+
+ :returns: The dummy request token string.
+
+ The dummy request token should be associated with a request token
+ secret such that get_request_token_secret(.., dummy_request_token)
+ returns a valid secret.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ @property
+ def dummy_access_token(self):
+ """Dummy access token used when an invalid token was supplied.
+
+ :returns: The dummy access token string.
+
+ The dummy access token should be associated with an access token
+ secret such that get_access_token_secret(.., dummy_access_token)
+ returns a valid secret.
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_client_secret(self, client_key, request):
+ """Retrieves the client secret associated with the client key.
+
+ :param client_key: The client/consumer key.
+ :param request: An oauthlib.common.Request object.
+ :returns: The client secret as a string.
+
+ This method must allow the use of a dummy client_key value.
+ Fetching the secret using the dummy key must take the same amount of
+ time as fetching a secret for a valid client::
+
+ # Unlikely to be near constant time as it uses two database
+ # lookups for a valid client, and only one for an invalid.
+ from your_datastore import ClientSecret
+ if ClientSecret.has(client_key):
+ return ClientSecret.get(client_key)
+ else:
+ return 'dummy'
+
+ # Aim to mimic number of latency inducing operations no matter
+ # whether the client is valid or not.
+ from your_datastore import ClientSecret
+ return ClientSecret.get(client_key, 'dummy')
+
+ Note that the returned key must be in plaintext.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_request_token_secret(self, client_key, token, request):
+ """Retrieves the shared secret associated with the request token.
+
+ :param client_key: The client/consumer key.
+ :param token: The request token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: The token secret as a string.
+
+ This method must allow the use of a dummy values and the running time
+ must be roughly equivalent to that of the running time of valid values::
+
+ # Unlikely to be near constant time as it uses two database
+ # lookups for a valid client, and only one for an invalid.
+ from your_datastore import RequestTokenSecret
+ if RequestTokenSecret.has(client_key):
+ return RequestTokenSecret.get((client_key, request_token))
+ else:
+ return 'dummy'
+
+ # Aim to mimic number of latency inducing operations no matter
+ # whether the client is valid or not.
+ from your_datastore import RequestTokenSecret
+ return ClientSecret.get((client_key, request_token), 'dummy')
+
+ Note that the returned key must be in plaintext.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_access_token_secret(self, client_key, token, request):
+ """Retrieves the shared secret associated with the access token.
+
+ :param client_key: The client/consumer key.
+ :param token: The access token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: The token secret as a string.
+
+ This method must allow the use of a dummy values and the running time
+ must be roughly equivalent to that of the running time of valid values::
+
+ # Unlikely to be near constant time as it uses two database
+ # lookups for a valid client, and only one for an invalid.
+ from your_datastore import AccessTokenSecret
+ if AccessTokenSecret.has(client_key):
+ return AccessTokenSecret.get((client_key, request_token))
+ else:
+ return 'dummy'
+
+ # Aim to mimic number of latency inducing operations no matter
+ # whether the client is valid or not.
+ from your_datastore import AccessTokenSecret
+ return ClientSecret.get((client_key, request_token), 'dummy')
+
+ Note that the returned key must be in plaintext.
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_default_realms(self, client_key, request):
+ """Get the default realms for a client.
+
+ :param client_key: The client/consumer key.
+ :param request: An oauthlib.common.Request object.
+ :returns: The list of default realms associated with the client.
+
+ The list of default realms will be set during client registration and
+ is outside the scope of OAuthLib.
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_realms(self, token, request):
+ """Get realms associated with a request token.
+
+ :param token: The request token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: The list of realms associated with the request token.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_redirect_uri(self, token, request):
+ """Get the redirect URI associated with a request token.
+
+ :param token: The request token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: The redirect URI associated with the request token.
+
+ It may be desirable to return a custom URI if the redirect is set to "oob".
+ In this case, the user will be redirected to the returned URI and at that
+ endpoint the verifier can be displayed.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_rsa_key(self, client_key, request):
+ """Retrieves a previously stored client provided RSA key.
+
+ :param client_key: The client/consumer key.
+ :param request: An oauthlib.common.Request object.
+ :returns: The rsa public key as a string.
+
+ This method must allow the use of a dummy client_key value. Fetching
+ the rsa key using the dummy key must take the same amount of time
+ as fetching a key for a valid client. The dummy key must also be of
+ the same bit length as client keys.
+
+ Note that the key must be returned in plaintext.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def invalidate_request_token(self, client_key, request_token, request):
+ """Invalidates a used request token.
+
+ :param client_key: The client/consumer key.
+ :param request_token: The request token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: The rsa public key as a string.
+
+ Per `Section 2.3`__ of the spec:
+
+ "The server MUST (...) ensure that the temporary
+ credentials have not expired or been used before."
+
+ .. _`Section 2.3`: http://tools.ietf.org/html/rfc5849#section-2.3
+
+ This method should ensure that provided token won't validate anymore.
+ It can be simply removing RequestToken from storage or setting
+ specific flag that makes it invalid (note that such flag should be
+ also validated during request token validation).
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_client_key(self, client_key, request):
+ """Validates that supplied client key is a registered and valid client.
+
+ :param client_key: The client/consumer key.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ Note that if the dummy client is supplied it should validate in same
+ or nearly the same amount of time as a valid one.
+
+ Ensure latency inducing tasks are mimiced even for dummy clients.
+ For example, use::
+
+ from your_datastore import Client
+ try:
+ return Client.exists(client_key, access_token)
+ except DoesNotExist:
+ return False
+
+ Rather than::
+
+ from your_datastore import Client
+ if access_token == self.dummy_access_token:
+ return False
+ else:
+ return Client.exists(client_key, access_token)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_request_token(self, client_key, token, request):
+ """Validates that supplied request token is registered and valid.
+
+ :param client_key: The client/consumer key.
+ :param token: The request token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ Note that if the dummy request_token is supplied it should validate in
+ the same nearly the same amount of time as a valid one.
+
+ Ensure latency inducing tasks are mimiced even for dummy clients.
+ For example, use::
+
+ from your_datastore import RequestToken
+ try:
+ return RequestToken.exists(client_key, access_token)
+ except DoesNotExist:
+ return False
+
+ Rather than::
+
+ from your_datastore import RequestToken
+ if access_token == self.dummy_access_token:
+ return False
+ else:
+ return RequestToken.exists(client_key, access_token)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_access_token(self, client_key, token, request):
+ """Validates that supplied access token is registered and valid.
+
+ :param client_key: The client/consumer key.
+ :param token: The access token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ Note that if the dummy access token is supplied it should validate in
+ the same or nearly the same amount of time as a valid one.
+
+ Ensure latency inducing tasks are mimiced even for dummy clients.
+ For example, use::
+
+ from your_datastore import AccessToken
+ try:
+ return AccessToken.exists(client_key, access_token)
+ except DoesNotExist:
+ return False
+
+ Rather than::
+
+ from your_datastore import AccessToken
+ if access_token == self.dummy_access_token:
+ return False
+ else:
+ return AccessToken.exists(client_key, access_token)
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+ request, request_token=None, access_token=None):
+ """Validates that the nonce has not been used before.
+
+ :param client_key: The client/consumer key.
+ :param timestamp: The ``oauth_timestamp`` parameter.
+ :param nonce: The ``oauth_nonce`` parameter.
+ :param request_token: Request token string, if any.
+ :param access_token: Access token string, if any.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ Per `Section 3.3`_ of the spec.
+
+ "A nonce is a random string, uniquely generated by the client to allow
+ the server to verify that a request has never been made before and
+ helps prevent replay attacks when requests are made over a non-secure
+ channel. The nonce value MUST be unique across all requests with the
+ same timestamp, client credentials, and token combinations."
+
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+
+ One of the first validation checks that will be made is for the validity
+ of the nonce and timestamp, which are associated with a client key and
+ possibly a token. If invalid then immediately fail the request
+ by returning False. If the nonce/timestamp pair has been used before and
+ you may just have detected a replay attack. Therefore it is an essential
+ part of OAuth security that you not allow nonce/timestamp reuse.
+ Note that this validation check is done before checking the validity of
+ the client and token.::
+
+ nonces_and_timestamps_database = [
+ (u'foo', 1234567890, u'rannoMstrInghere', u'bar')
+ ]
+
+ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+ request_token=None, access_token=None):
+
+ return ((client_key, timestamp, nonce, request_token or access_token)
+ not in self.nonces_and_timestamps_database)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_redirect_uri(self, client_key, redirect_uri, request):
+ """Validates the client supplied redirection URI.
+
+ :param client_key: The client/consumer key.
+ :param redirect_uri: The URI the client which to redirect back to after
+ authorization is successful.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ It is highly recommended that OAuth providers require their clients
+ to register all redirection URIs prior to using them in requests and
+ register them as absolute URIs. See `CWE-601`_ for more information
+ about open redirection attacks.
+
+ By requiring registration of all redirection URIs it should be
+ straightforward for the provider to verify whether the supplied
+ redirect_uri is valid or not.
+
+ Alternatively per `Section 2.1`_ of the spec:
+
+ "If the client is unable to receive callbacks or a callback URI has
+ been established via other means, the parameter value MUST be set to
+ "oob" (case sensitive), to indicate an out-of-band configuration."
+
+ .. _`CWE-601`: http://cwe.mitre.org/top25/index.html#CWE-601
+ .. _`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_requested_realms(self, client_key, realms, request):
+ """Validates that the client may request access to the realm.
+
+ :param client_key: The client/consumer key.
+ :param realms: The list of realms that client is requesting access to.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ This method is invoked when obtaining a request token and should
+ tie a realm to the request token and after user authorization
+ this realm restriction should transfer to the access token.
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_realms(self, client_key, token, request, uri=None,
+ realms=None):
+ """Validates access to the request realm.
+
+ :param client_key: The client/consumer key.
+ :param token: A request token string.
+ :param request: An oauthlib.common.Request object.
+ :param uri: The URI the realms is protecting.
+ :param realms: A list of realms that must have been granted to
+ the access token.
+ :returns: True or False
+
+ How providers choose to use the realm parameter is outside the OAuth
+ specification but it is commonly used to restrict access to a subset
+ of protected resources such as "photos".
+
+ realms is a convenience parameter which can be used to provide
+ a per view method pre-defined list of allowed realms.
+
+ Can be as simple as::
+
+ from your_datastore import RequestToken
+ request_token = RequestToken.get(token, None)
+
+ if not request_token:
+ return False
+ return set(request_token.realms).issuperset(set(realms))
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def validate_verifier(self, client_key, token, verifier, request):
+ """Validates a verification code.
+
+ :param client_key: The client/consumer key.
+ :param token: A request token string.
+ :param verifier: The authorization verifier string.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ OAuth providers issue a verification code to clients after the
+ resource owner authorizes access. This code is used by the client to
+ obtain token credentials and the provider must verify that the
+ verifier is valid and associated with the client as well as the
+ resource owner.
+
+ Verifier validation should be done in near constant time
+ (to avoid verifier enumeration). To achieve this we need a
+ constant time string comparison which is provided by OAuthLib
+ in ``oauthlib.common.safe_string_equals``::
+
+ from your_datastore import Verifier
+ correct_verifier = Verifier.get(client_key, request_token)
+ from oauthlib.common import safe_string_equals
+ return safe_string_equals(verifier, correct_verifier)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def verify_request_token(self, token, request):
+ """Verify that the given OAuth1 request token is valid.
+
+ :param token: A request token string.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ This method is used only in AuthorizationEndpoint to check whether the
+ oauth_token given in the authorization URL is valid or not.
+ This request is not signed and thus similar ``validate_request_token``
+ method can not be used.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def verify_realms(self, token, realms, request):
+ """Verify authorized realms to see if they match those given to token.
+
+ :param token: An access token string.
+ :param realms: A list of realms the client attempts to access.
+ :param request: An oauthlib.common.Request object.
+ :returns: True or False
+
+ This prevents the list of authorized realms sent by the client during
+ the authorization step to be altered to include realms outside what
+ was bound with the request token.
+
+ Can be as simple as::
+
+ valid_realms = self.get_realms(token)
+ return all((r in valid_realms for r in realms))
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def save_access_token(self, token, request):
+ """Save an OAuth1 access token.
+
+ :param token: A dict with token credentials.
+ :param request: An oauthlib.common.Request object.
+
+ The token dictionary will at minimum include
+
+ * ``oauth_token`` the access token string.
+ * ``oauth_token_secret`` the token specific secret used in signing.
+ * ``oauth_authorized_realms`` a space separated list of realms.
+
+ Client key can be obtained from ``request.client_key``.
+
+ The list of realms (not joined string) can be obtained from
+ ``request.realm``.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def save_request_token(self, token, request):
+ """Save an OAuth1 request token.
+
+ :param token: A dict with token credentials.
+ :param request: An oauthlib.common.Request object.
+
+ The token dictionary will at minimum include
+
+ * ``oauth_token`` the request token string.
+ * ``oauth_token_secret`` the token specific secret used in signing.
+ * ``oauth_callback_confirmed`` the string ``true``.
+
+ Client key can be obtained from ``request.client_key``.
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def save_verifier(self, token, verifier, request):
+ """Associate an authorization verifier with a request token.
+
+ :param token: A request token string.
+ :param verifier A dictionary containing the oauth_verifier and
+ oauth_token
+ :param request: An oauthlib.common.Request object.
+
+ We need to associate verifiers with tokens for validation during the
+ access token request.
+
+ Note that unlike save_x_token token here is the ``oauth_token`` token
+ string from the request token saved previously.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise NotImplementedError("Subclasses must implement this function.")
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.signature
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module represents a direct implementation of `section 3.4`_ of the spec.
+
+Terminology:
+ * Client: software interfacing with an OAuth API
+ * Server: the API provider
+ * Resource Owner: the user who is granting authorization to the client
+
+Steps for signing a request:
+
+1. Collect parameters from the uri query, auth header, & body
+2. Normalize those parameters
+3. Normalize the uri
+4. Pass the normalized uri, normalized parameters, and http method to
+ construct the base string
+5. Pass the base string and any keys needed to a signing function
+
+.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+"""
+from __future__ import absolute_import, unicode_literals
+
+import binascii
+import hashlib
+import hmac
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+from . import utils
+from oauthlib.common import urldecode, extract_params, safe_string_equals
+from oauthlib.common import bytes_type, unicode_type
+
+
+def construct_base_string(http_method, base_string_uri,
+ normalized_encoded_request_parameters):
+ """**String Construction**
+ Per `section 3.4.1.1`_ of the spec.
+
+ For example, the HTTP request::
+
+ POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
+ Host: example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="9djdj82h48djs9d2",
+ oauth_token="kkk9d7dh3k39sjv7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="137131201",
+ oauth_nonce="7d8f3e4a",
+ oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
+
+ c2&a3=2+q
+
+ is represented by the following signature base string (line breaks
+ are for display purposes only)::
+
+ POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
+ %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
+ key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
+ ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
+ 9d7dh3k39sjv7
+
+ .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ """
+
+ # The signature base string is constructed by concatenating together,
+ # in order, the following HTTP request elements:
+
+ # 1. The HTTP request method in uppercase. For example: "HEAD",
+ # "GET", "POST", etc. If the request uses a custom HTTP method, it
+ # MUST be encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ base_string = utils.escape(http_method.upper())
+
+ # 2. An "&" character (ASCII code 38).
+ base_string += '&'
+
+ # 3. The base string URI from `Section 3.4.1.2`_, after being encoded
+ # (`Section 3.6`_).
+ #
+ # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+ base_string += utils.escape(base_string_uri)
+
+ # 4. An "&" character (ASCII code 38).
+ base_string += '&'
+
+ # 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
+ # being encoded (`Section 3.6`).
+ #
+ # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+ base_string += utils.escape(normalized_encoded_request_parameters)
+
+ return base_string
+
+
+def normalize_base_string_uri(uri, host=None):
+ """**Base String URI**
+ Per `section 3.4.1.2`_ of the spec.
+
+ For example, the HTTP request::
+
+ GET /r%20v/X?id=123 HTTP/1.1
+ Host: EXAMPLE.COM:80
+
+ is represented by the base string URI: "http://example.com/r%20v/X".
+
+ In another example, the HTTPS request::
+
+ GET /?q=1 HTTP/1.1
+ Host: www.example.net:8080
+
+ is represented by the base string URI: "https://www.example.net:8080/".
+
+ .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+
+ The host argument overrides the netloc part of the uri argument.
+ """
+ if not isinstance(uri, unicode_type):
+ raise ValueError('uri must be a unicode object.')
+
+ # FIXME: urlparse does not support unicode
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
+
+ # The scheme, authority, and path of the request resource URI `RFC3986`
+ # are included by constructing an "http" or "https" URI representing
+ # the request resource (without the query or fragment) as follows:
+ #
+ # .. _`RFC3986`: http://tools.ietf.org/html/rfc3986
+
+ if not scheme or not netloc:
+ raise ValueError('uri must include a scheme and netloc')
+
+ # Per `RFC 2616 section 5.1.2`_:
+ #
+ # Note that the absolute path cannot be empty; if none is present in
+ # the original URI, it MUST be given as "/" (the server root).
+ #
+ # .. _`RFC 2616 section 5.1.2`: http://tools.ietf.org/html/rfc2616#section-5.1.2
+ if not path:
+ path = '/'
+
+ # 1. The scheme and host MUST be in lowercase.
+ scheme = scheme.lower()
+ netloc = netloc.lower()
+
+ # 2. The host and port values MUST match the content of the HTTP
+ # request "Host" header field.
+ if host is not None:
+ netloc = host.lower()
+
+ # 3. The port MUST be included if it is not the default port for the
+ # scheme, and MUST be excluded if it is the default. Specifically,
+ # the port MUST be excluded when making an HTTP request `RFC2616`_
+ # to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
+ # All other non-default port numbers MUST be included.
+ #
+ # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616
+ # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818
+ default_ports = (
+ ('http', '80'),
+ ('https', '443'),
+ )
+ if ':' in netloc:
+ host, port = netloc.split(':', 1)
+ if (scheme, port) in default_ports:
+ netloc = host
+
+ return urlparse.urlunparse((scheme, netloc, path, params, '', ''))
+
+
+# ** Request Parameters **
+#
+# Per `section 3.4.1.3`_ of the spec.
+#
+# In order to guarantee a consistent and reproducible representation of
+# the request parameters, the parameters are collected and decoded to
+# their original decoded form. They are then sorted and encoded in a
+# particular manner that is often different from their original
+# encoding scheme, and concatenated into a single string.
+#
+# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+def collect_parameters(uri_query='', body=[], headers=None,
+ exclude_oauth_signature=True, with_realm=False):
+ """**Parameter Sources**
+
+ Parameters starting with `oauth_` will be unescaped.
+
+ Body parameters must be supplied as a dict, a list of 2-tuples, or a
+ formencoded query string.
+
+ Headers must be supplied as a dict.
+
+ Per `section 3.4.1.3.1`_ of the spec.
+
+ For example, the HTTP request::
+
+ POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
+ Host: example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="9djdj82h48djs9d2",
+ oauth_token="kkk9d7dh3k39sjv7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="137131201",
+ oauth_nonce="7d8f3e4a",
+ oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"
+
+ c2&a3=2+q
+
+ contains the following (fully decoded) parameters used in the
+ signature base sting::
+
+ +------------------------+------------------+
+ | Name | Value |
+ +------------------------+------------------+
+ | b5 | =%3D |
+ | a3 | a |
+ | c@ | |
+ | a2 | r b |
+ | oauth_consumer_key | 9djdj82h48djs9d2 |
+ | oauth_token | kkk9d7dh3k39sjv7 |
+ | oauth_signature_method | HMAC-SHA1 |
+ | oauth_timestamp | 137131201 |
+ | oauth_nonce | 7d8f3e4a |
+ | c2 | |
+ | a3 | 2 q |
+ +------------------------+------------------+
+
+ Note that the value of "b5" is "=%3D" and not "==". Both "c@" and
+ "c2" have empty values. While the encoding rules specified in this
+ specification for the purpose of constructing the signature base
+ string exclude the use of a "+" character (ASCII code 43) to
+ represent an encoded space character (ASCII code 32), this practice
+ is widely used in "application/x-www-form-urlencoded" encoded values,
+ and MUST be properly decoded, as demonstrated by one of the "a3"
+ parameter instances (the "a3" parameter is used twice in this
+ request).
+
+ .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ """
+ headers = headers or {}
+ params = []
+
+ # The parameters from the following sources are collected into a single
+ # list of name/value pairs:
+
+ # * The query component of the HTTP request URI as defined by
+ # `RFC3986, Section 3.4`_. The query component is parsed into a list
+ # of name/value pairs by treating it as an
+ # "application/x-www-form-urlencoded" string, separating the names
+ # and values and decoding them as defined by
+ # `W3C.REC-html40-19980424`_, Section 17.13.4.
+ #
+ # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
+ # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+ if uri_query:
+ params.extend(urldecode(uri_query))
+
+ # * The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
+ # present. The header's content is parsed into a list of name/value
+ # pairs excluding the "realm" parameter if present. The parameter
+ # values are decoded as defined by `Section 3.5.1`_.
+ #
+ # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+ if headers:
+ headers_lower = dict((k.lower(), v) for k, v in headers.items())
+ authorization_header = headers_lower.get('authorization')
+ if authorization_header is not None:
+ params.extend([i for i in utils.parse_authorization_header(
+ authorization_header) if with_realm or i[0] != 'realm'])
+
+ # * The HTTP request entity-body, but only if all of the following
+ # conditions are met:
+ # * The entity-body is single-part.
+ #
+ # * The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # `W3C.REC-html40-19980424`_.
+
+ # * The HTTP request entity-header includes the "Content-Type"
+ # header field set to "application/x-www-form-urlencoded".
+ #
+ # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+
+ # TODO: enforce header param inclusion conditions
+ bodyparams = extract_params(body) or []
+ params.extend(bodyparams)
+
+ # ensure all oauth params are unescaped
+ unescaped_params = []
+ for k, v in params:
+ if k.startswith('oauth_'):
+ v = utils.unescape(v)
+ unescaped_params.append((k, v))
+
+ # The "oauth_signature" parameter MUST be excluded from the signature
+ # base string if present.
+ if exclude_oauth_signature:
+ unescaped_params = list(filter(lambda i: i[0] != 'oauth_signature',
+ unescaped_params))
+
+ return unescaped_params
+
+
+def normalize_parameters(params):
+ """**Parameters Normalization**
+ Per `section 3.4.1.3.2`_ of the spec.
+
+ For example, the list of parameters from the previous section would
+ be normalized as follows:
+
+ Encoded::
+
+ +------------------------+------------------+
+ | Name | Value |
+ +------------------------+------------------+
+ | b5 | %3D%253D |
+ | a3 | a |
+ | c%40 | |
+ | a2 | r%20b |
+ | oauth_consumer_key | 9djdj82h48djs9d2 |
+ | oauth_token | kkk9d7dh3k39sjv7 |
+ | oauth_signature_method | HMAC-SHA1 |
+ | oauth_timestamp | 137131201 |
+ | oauth_nonce | 7d8f3e4a |
+ | c2 | |
+ | a3 | 2%20q |
+ +------------------------+------------------+
+
+ Sorted::
+
+ +------------------------+------------------+
+ | Name | Value |
+ +------------------------+------------------+
+ | a2 | r%20b |
+ | a3 | 2%20q |
+ | a3 | a |
+ | b5 | %3D%253D |
+ | c%40 | |
+ | c2 | |
+ | oauth_consumer_key | 9djdj82h48djs9d2 |
+ | oauth_nonce | 7d8f3e4a |
+ | oauth_signature_method | HMAC-SHA1 |
+ | oauth_timestamp | 137131201 |
+ | oauth_token | kkk9d7dh3k39sjv7 |
+ +------------------------+------------------+
+
+ Concatenated Pairs::
+
+ +-------------------------------------+
+ | Name=Value |
+ +-------------------------------------+
+ | a2=r%20b |
+ | a3=2%20q |
+ | a3=a |
+ | b5=%3D%253D |
+ | c%40= |
+ | c2= |
+ | oauth_consumer_key=9djdj82h48djs9d2 |
+ | oauth_nonce=7d8f3e4a |
+ | oauth_signature_method=HMAC-SHA1 |
+ | oauth_timestamp=137131201 |
+ | oauth_token=kkk9d7dh3k39sjv7 |
+ +-------------------------------------+
+
+ and concatenated together into a single string (line breaks are for
+ display purposes only)::
+
+ a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
+ dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
+ &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
+
+ .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ """
+
+ # The parameters collected in `Section 3.4.1.3`_ are normalized into a
+ # single string as follows:
+ #
+ # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+ # 1. First, the name and value of each parameter are encoded
+ # (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
+
+ # 2. The parameters are sorted by name, using ascending byte value
+ # ordering. If two or more parameters share the same name, they
+ # are sorted by their value.
+ key_values.sort()
+
+ # 3. The name of each parameter is concatenated to its corresponding
+ # value using an "=" character (ASCII code 61) as a separator, even
+ # if the value is empty.
+ parameter_parts = ['{0}={1}'.format(k, v) for k, v in key_values]
+
+ # 4. The sorted name/value pairs are concatenated together into a
+ # single string by using an "&" character (ASCII code 38) as
+ # separator.
+ return '&'.join(parameter_parts)
+
+
+def sign_hmac_sha1_with_client(base_string, client):
+ return sign_hmac_sha1(base_string,
+ client.client_secret,
+ client.resource_owner_secret
+ )
+
+
+def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
+ """**HMAC-SHA1**
+
+ The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
+ algorithm as defined in `RFC2104`_::
+
+ digest = HMAC-SHA1 (key, text)
+
+ Per `section 3.4.2`_ of the spec.
+
+ .. _`RFC2104`: http://tools.ietf.org/html/rfc2104
+ .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
+ """
+
+ # The HMAC-SHA1 function variables are used in following way:
+
+ # text is set to the value of the signature base string from
+ # `Section 3.4.1.1`_.
+ #
+ # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ text = base_string
+
+ # key is set to the concatenated values of:
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ key = utils.escape(client_secret or '')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included
+ # even when either secret is empty.
+ key += '&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ key += utils.escape(resource_owner_secret or '')
+
+ # FIXME: HMAC does not support unicode!
+ key_utf8 = key.encode('utf-8')
+ text_utf8 = text.encode('utf-8')
+ signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
+
+ # digest is used to set the value of the "oauth_signature" protocol
+ # parameter, after the result octet string is base64-encoded
+ # per `RFC2045, Section 6.8`.
+ #
+ # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8
+ return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+_jwtrs1 = None
+
+#jwt has some nice pycrypto/cryptography abstractions
+def _jwt_rs1_signing_algorithm():
+ global _jwtrs1
+ if _jwtrs1 is None:
+ import jwt.algorithms as jwtalgo
+ _jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1)
+ return _jwtrs1
+
+def sign_rsa_sha1(base_string, rsa_private_key):
+ """**RSA-SHA1**
+
+ Per `section 3.4.3`_ of the spec.
+
+ The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
+ algorithm as defined in `RFC3447, Section 8.2`_ (also known as
+ PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To
+ use this method, the client MUST have established client credentials
+ with the server that included its RSA public key (in a manner that is
+ beyond the scope of this specification).
+
+ .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
+ .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
+
+ """
+ if isinstance(base_string, unicode_type):
+ base_string = base_string.encode('utf-8')
+ # TODO: finish RSA documentation
+ alg = _jwt_rs1_signing_algorithm()
+ key = _prepare_key_plus(alg, rsa_private_key)
+ s=alg.sign(base_string, key)
+ return binascii.b2a_base64(s)[:-1].decode('utf-8')
+
+
+def sign_rsa_sha1_with_client(base_string, client):
+ return sign_rsa_sha1(base_string, client.rsa_key)
+
+
+def sign_plaintext(client_secret, resource_owner_secret):
+ """Sign a request using plaintext.
+
+ Per `section 3.4.4`_ of the spec.
+
+ The "PLAINTEXT" method does not employ a signature algorithm. It
+ MUST be used with a transport-layer mechanism such as TLS or SSL (or
+ sent over a secure channel with equivalent protections). It does not
+ utilize the signature base string or the "oauth_timestamp" and
+ "oauth_nonce" parameters.
+
+ .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4
+
+ """
+
+ # The "oauth_signature" protocol parameter is set to the concatenated
+ # value of:
+
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ signature = utils.escape(client_secret or '')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included even
+ # when either secret is empty.
+ signature += '&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ signature += utils.escape(resource_owner_secret or '')
+
+ return signature
+
+
+def sign_plaintext_with_client(base_string, client):
+ return sign_plaintext(client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha1(request, client_secret=None,
+ resource_owner_secret=None):
+ """Verify a HMAC-SHA1 signature.
+
+ Per `section 3.4`_ of the spec.
+
+ .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+
+ To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+ attribute MUST be an absolute URI whose netloc part identifies the
+ origin server or gateway on which the resource resides. Any Host
+ item of the request argument's headers dict attribute will be
+ ignored.
+
+ .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
+
+ """
+ norm_params = normalize_parameters(request.params)
+ uri = normalize_base_string_uri(request.uri)
+ base_string = construct_base_string(request.http_method, uri, norm_params)
+ signature = sign_hmac_sha1(base_string, client_secret,
+ resource_owner_secret)
+ return safe_string_equals(signature, request.signature)
+
+def _prepare_key_plus(alg, keystr):
+ if isinstance(keystr, bytes_type):
+ keystr = keystr.decode('utf-8')
+ return alg.prepare_key(keystr)
+
+def verify_rsa_sha1(request, rsa_public_key):
+ """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.
+
+ Per `section 3.4.3`_ of the spec.
+
+ Note this method requires the jwt and cryptography libraries.
+
+ .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
+
+ To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+ attribute MUST be an absolute URI whose netloc part identifies the
+ origin server or gateway on which the resource resides. Any Host
+ item of the request argument's headers dict attribute will be
+ ignored.
+
+ .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
+ """
+ norm_params = normalize_parameters(request.params)
+ uri = normalize_base_string_uri(request.uri)
+ message = construct_base_string(request.http_method, uri, norm_params).encode('utf-8')
+ sig = binascii.a2b_base64(request.signature.encode('utf-8'))
+
+ alg = _jwt_rs1_signing_algorithm()
+ key = _prepare_key_plus(alg, rsa_public_key)
+ return alg.verify(message, key, sig)
+
+
+def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
+ """Verify a PLAINTEXT signature.
+
+ Per `section 3.4`_ of the spec.
+
+ .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+ """
+ signature = sign_plaintext(client_secret, resource_owner_secret)
+ return safe_string_equals(signature, request.signature)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth
+spec.
+"""
+from __future__ import absolute_import, unicode_literals
+
+try:
+ import urllib2
+except ImportError:
+ import urllib.request as urllib2
+
+from oauthlib.common import quote, unquote, bytes_type, unicode_type
+
+UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ '0123456789')
+
+
+def filter_params(target):
+ """Decorator which filters params to remove non-oauth_* parameters
+
+ Assumes the decorated method takes a params dict or list of tuples as its
+ first argument.
+ """
+ def wrapper(params, *args, **kwargs):
+ params = filter_oauth_params(params)
+ return target(params, *args, **kwargs)
+
+ wrapper.__doc__ = target.__doc__
+ return wrapper
+
+
+def filter_oauth_params(params):
+ """Removes all non oauth parameters from a dict or a list of params."""
+ is_oauth = lambda kv: kv[0].startswith("oauth_")
+ if isinstance(params, dict):
+ return list(filter(is_oauth, list(params.items())))
+ else:
+ return list(filter(is_oauth, params))
+
+
+def escape(u):
+ """Escape a unicode string in an OAuth-compatible fashion.
+
+ Per `section 3.6`_ of the spec.
+
+ .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+
+ """
+ if not isinstance(u, unicode_type):
+ raise ValueError('Only unicode objects are escapable. ' +
+ 'Got %s of type %s.' % (u, type(u)))
+ # Letters, digits, and the characters '_.-' are already treated as safe
+ # by urllib.quote(). We need to add '~' to fully support rfc5849.
+ return quote(u, safe=b'~')
+
+
+def unescape(u):
+ if not isinstance(u, unicode_type):
+ raise ValueError('Only unicode objects are unescapable.')
+ return unquote(u)
+
+
+def parse_keqv_list(l):
+ """A unicode-safe version of urllib2.parse_keqv_list"""
+ # With Python 2.6, parse_http_list handles unicode fine
+ return urllib2.parse_keqv_list(l)
+
+
+def parse_http_list(u):
+ """A unicode-safe version of urllib2.parse_http_list"""
+ # With Python 2.6, parse_http_list handles unicode fine
+ return urllib2.parse_http_list(u)
+
+
+def parse_authorization_header(authorization_header):
+ """Parse an OAuth authorization header into a list of 2-tuples"""
+ auth_scheme = 'OAuth '.lower()
+ if authorization_header[:len(auth_scheme)].lower().startswith(auth_scheme):
+ items = parse_http_list(authorization_header[len(auth_scheme):])
+ try:
+ return list(parse_keqv_list(items).items())
+ except (IndexError, ValueError):
+ pass
+ raise ValueError('Malformed authorization header')
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 2.0 Client
+and Server classes.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .rfc6749.clients import Client
+from .rfc6749.clients import WebApplicationClient
+from .rfc6749.clients import MobileApplicationClient
+from .rfc6749.clients import LegacyApplicationClient
+from .rfc6749.clients import BackendApplicationClient
+from .rfc6749.clients import ServiceApplicationClient
+from .rfc6749.endpoints import AuthorizationEndpoint
+from .rfc6749.endpoints import TokenEndpoint
+from .rfc6749.endpoints import ResourceEndpoint
+from .rfc6749.endpoints import RevocationEndpoint
+from .rfc6749.endpoints import Server
+from .rfc6749.endpoints import WebApplicationServer
+from .rfc6749.endpoints import MobileApplicationServer
+from .rfc6749.endpoints import LegacyApplicationServer
+from .rfc6749.endpoints import BackendApplicationServer
+from .rfc6749.errors import *
+from .rfc6749.grant_types import AuthorizationCodeGrant
+from .rfc6749.grant_types import ImplicitGrant
+from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant
+from .rfc6749.grant_types import ClientCredentialsGrant
+from .rfc6749.grant_types import RefreshTokenGrant
+from .rfc6749.request_validator import RequestValidator
+from .rfc6749.tokens import BearerToken, OAuth2Token
+from .rfc6749.utils import is_secure_transport
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import functools
+import logging
+
+from .errors import TemporarilyUnavailableError, ServerError
+from .errors import FatalClientError, OAuth2Error
+
+
+log = logging.getLogger(__name__)
+
+
+class BaseEndpoint(object):
+
+ def __init__(self):
+ self._available = True
+ self._catch_errors = False
+
+ @property
+ def available(self):
+ return self._available
+
+ @available.setter
+ def available(self, available):
+ self._available = available
+
+ @property
+ def catch_errors(self):
+ return self._catch_errors
+
+ @catch_errors.setter
+ def catch_errors(self, catch_errors):
+ self._catch_errors = catch_errors
+
+
+def catch_errors_and_unavailability(f):
+ @functools.wraps(f)
+ def wrapper(endpoint, uri, *args, **kwargs):
+ if not endpoint.available:
+ e = TemporarilyUnavailableError()
+ log.info('Endpoint unavailable, ignoring request %s.' % uri)
+ return {}, e.json, 503
+
+ if endpoint.catch_errors:
+ try:
+ return f(endpoint, uri, *args, **kwargs)
+ except OAuth2Error:
+ raise
+ except FatalClientError:
+ raise
+ except Exception as e:
+ error = ServerError()
+ log.warning(
+ 'Exception caught while processing request, %s.' % e)
+ return {}, error.json, 500
+ else:
+ return f(endpoint, uri, *args, **kwargs)
+ return wrapper
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import *
+from .web_application import WebApplicationClient
+from .mobile_application import MobileApplicationClient
+from .legacy_application import LegacyApplicationClient
+from .backend_application import BackendApplicationClient
+from .service_application import ServiceApplicationClient
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_token_request
+from ..parameters import parse_token_response
+
+
+class BackendApplicationClient(Client):
+
+ """A public client utilizing the client credentials grant workflow.
+
+ The client can request an access token using only its client
+ credentials (or other supported means of authentication) when the
+ client is requesting access to the protected resources under its
+ control, or those of another resource owner which has been previously
+ arranged with the authorization server (the method of which is beyond
+ the scope of this specification).
+
+ The client credentials grant type MUST only be used by confidential
+ clients.
+
+ Since the client authentication is used as the authorization grant,
+ no additional authorization request is needed.
+ """
+
+ def prepare_request_body(self, body='', scope=None, **kwargs):
+ """Add the client credentials to the request body.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format per `Appendix B`_ in the HTTP request entity-body:
+
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+ :param kwargs: Extra credentials to include in the token request.
+
+ The client MUST authenticate with the authorization server as
+ described in `Section 3.2.1`_.
+
+ The prepared body will include all provided credentials as well as
+ the ``grant_type`` parameter set to ``client_credentials``::
+
+ >>> from oauthlib.oauth2 import BackendApplicationClient
+ >>> client = BackendApplicationClient('your_id')
+ >>> client.prepare_request_body(scope=['hello', 'world'])
+ 'grant_type=client_credentials&scope=hello+world'
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ return prepare_token_request('client_credentials', body=body,
+ scope=scope, **kwargs)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import time
+
+from oauthlib.common import generate_token
+from oauthlib.oauth2.rfc6749 import tokens
+from oauthlib.oauth2.rfc6749.parameters import parse_token_response
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_revocation_request
+from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
+from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
+from oauthlib.oauth2.rfc6749.utils import is_secure_transport
+
+
+AUTH_HEADER = 'auth_header'
+URI_QUERY = 'query'
+BODY = 'body'
+
+FORM_ENC_HEADERS = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+}
+
+class Client(object):
+
+ """Base OAuth2 client responsible for access token management.
+
+ This class also acts as a generic interface providing methods common to all
+ client types such as ``prepare_authorization_request`` and
+ ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
+ the recommended way of interacting with clients (as opposed to the abstract
+ prepare uri/body/etc methods). They are recommended over the older set
+ because they are easier to use (more consistent) and add a few additional
+ security checks, such as HTTPS and state checking.
+
+ Some of these methods require further implementation only provided by the
+ specific purpose clients such as
+ :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
+ seek to use the client class matching the OAuth workflow you need. For
+ Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
+
+ """
+
+ def __init__(self, client_id,
+ default_token_placement=AUTH_HEADER,
+ token_type='Bearer',
+ access_token=None,
+ refresh_token=None,
+ mac_key=None,
+ mac_algorithm=None,
+ token=None,
+ scope=None,
+ state=None,
+ redirect_url=None,
+ state_generator=generate_token,
+ **kwargs):
+ """Initialize a client with commonly used attributes.
+
+ :param client_id: Client identifier given by the OAuth provider upon
+ registration.
+
+ :param default_token_placement: Tokens can be supplied in the Authorization
+ header (default), the URL query component (``query``) or the request
+ body (``body``).
+
+ :param token_type: OAuth 2 token type. Defaults to Bearer. Change this
+ if you specify the ``access_token`` parameter and know it is of a
+ different token type, such as a MAC, JWT or SAML token. Can
+ also be supplied as ``token_type`` inside the ``token`` dict parameter.
+
+ :param access_token: An access token (string) used to authenticate
+ requests to protected resources. Can also be supplied inside the
+ ``token`` dict parameter.
+
+ :param refresh_token: A refresh token (string) used to refresh expired
+ tokens. Can also be supplide inside the ``token`` dict parameter.
+
+ :param mac_key: Encryption key used with MAC tokens.
+
+ :param mac_algorithm: Hashing algorithm for MAC tokens.
+
+ :param token: A dict of token attributes such as ``access_token``,
+ ``token_type`` and ``expires_at``.
+
+ :param scope: A list of default scopes to request authorization for.
+
+ :param state: A CSRF protection string used during authorization.
+
+ :param redirect_url: The redirection endpoint on the client side to which
+ the user returns after authorization.
+
+ :param state_generator: A no argument state generation callable. Defaults
+ to :py:meth:`oauthlib.common.generate_token`.
+ """
+
+ self.client_id = client_id
+ self.default_token_placement = default_token_placement
+ self.token_type = token_type
+ self.access_token = access_token
+ self.refresh_token = refresh_token
+ self.mac_key = mac_key
+ self.mac_algorithm = mac_algorithm
+ self.token = token or {}
+ self.scope = scope
+ self.state_generator = state_generator
+ self.state = state
+ self.redirect_url = redirect_url
+ self._expires_at = None
+ self._populate_attributes(self.token)
+
+ @property
+ def token_types(self):
+ """Supported token types and their respective methods
+
+ Additional tokens can be supported by extending this dictionary.
+
+ The Bearer token spec is stable and safe to use.
+
+ The MAC token spec is not yet stable and support for MAC tokens
+ is experimental and currently matching version 00 of the spec.
+ """
+ return {
+ 'Bearer': self._add_bearer_token,
+ 'MAC': self._add_mac_token
+ }
+
+ def prepare_request_uri(self, *args, **kwargs):
+ """Abstract method used to create request URIs."""
+ raise NotImplementedError("Must be implemented by inheriting classes.")
+
+ def prepare_request_body(self, *args, **kwargs):
+ """Abstract method used to create request bodies."""
+ raise NotImplementedError("Must be implemented by inheriting classes.")
+
+ def parse_request_uri_response(self, *args, **kwargs):
+ """Abstract method used to parse redirection responses."""
+
+ def add_token(self, uri, http_method='GET', body=None, headers=None,
+ token_placement=None, **kwargs):
+ """Add token to the request uri, body or authorization header.
+
+ The access token type provides the client with the information
+ required to successfully utilize the access token to make a protected
+ resource request (along with type-specific attributes). The client
+ MUST NOT use an access token if it does not understand the token
+ type.
+
+ For example, the "bearer" token type defined in
+ [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
+ token string in the request:
+
+ .. code-block:: http
+
+ GET /resource/1 HTTP/1.1
+ Host: example.com
+ Authorization: Bearer mF_9.B5f-4.1JqM
+
+ while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
+ utilized by issuing a MAC key together with the access token which is
+ used to sign certain components of the HTTP requests:
+
+ .. code-block:: http
+
+ GET /resource/1 HTTP/1.1
+ Host: example.com
+ Authorization: MAC id="h480djs93hd8",
+ nonce="274312:dj83hs9s",
+ mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
+
+ .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2
+ .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ token_placement = token_placement or self.default_token_placement
+
+ case_insensitive_token_types = dict(
+ (k.lower(), v) for k, v in self.token_types.items())
+ if not self.token_type.lower() in case_insensitive_token_types:
+ raise ValueError("Unsupported token type: %s" % self.token_type)
+
+ if not self.access_token:
+ raise ValueError("Missing access token.")
+
+ if self._expires_at and self._expires_at < time.time():
+ raise TokenExpiredError()
+
+ return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
+ headers, token_placement, **kwargs)
+
+ def prepare_authorization_request(self, authorization_url, state=None,
+ redirect_url=None, scope=None, **kwargs):
+ """Prepare the authorization request.
+
+ This is the first step in many OAuth flows in which the user is
+ redirected to a certain authorization URL. This method adds
+ required parameters to the authorization URL.
+
+ :param authorization_url: Provider authorization endpoint URL.
+
+ :param state: CSRF protection string. Will be automatically created if
+ not provided. The generated state is available via the ``state``
+ attribute. Clients should verify that the state is unchanged and
+ present in the authorization response. This verification is done
+ automatically if using the ``authorization_response`` parameter
+ with ``prepare_token_request``.
+
+ :param redirect_url: Redirect URL to which the user will be returned
+ after authorization. Must be provided unless previously setup with
+ the provider. If provided then it must also be provided in the
+ token request.
+
+ :param kwargs: Additional parameters to included in the request.
+
+ :returns: The prepared request tuple with (url, headers, body).
+ """
+ if not is_secure_transport(authorization_url):
+ raise InsecureTransportError()
+
+ self.state = state or self.state_generator()
+ self.redirect_url = redirect_url or self.redirect_url
+ self.scope = scope or self.scope
+ auth_url = self.prepare_request_uri(
+ authorization_url, redirect_uri=self.redirect_uri,
+ scope=self.scope, state=self.state, **kwargs)
+ return auth_url, FORM_ENC_HEADERS, ''
+
+ def prepare_token_request(self, token_url, authorization_response=None,
+ redirect_url=None, state=None, body='', **kwargs):
+ """Prepare a token creation request.
+
+ Note that these requests usually require client authentication, either
+ by including client_id or a set of provider specific authentication
+ credentials.
+
+ :param token_url: Provider token creation endpoint URL.
+
+ :param authorization_response: The full redirection URL string, i.e.
+ the location to which the user was redirected after successfull
+ authorization. Used to mine credentials needed to obtain a token
+ in this step, such as authorization code.
+
+ :param redirect_url: The redirect_url supplied with the authorization
+ request (if there was one).
+
+ :param body: Request body (URL encoded string).
+
+ :param kwargs: Additional parameters to included in the request.
+
+ :returns: The prepared request tuple with (url, headers, body).
+ """
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ state = state or self.state
+ if authorization_response:
+ self.parse_request_uri_response(
+ authorization_response, state=state)
+ self.redirect_url = redirect_url or self.redirect_url
+ body = self.prepare_request_body(body=body,
+ redirect_uri=self.redirect_url, **kwargs)
+
+ return token_url, FORM_ENC_HEADERS, body
+
+ def prepare_refresh_token_request(self, token_url, refresh_token=None,
+ body='', scope=None, **kwargs):
+ """Prepare an access token refresh request.
+
+ Expired access tokens can be replaced by new access tokens without
+ going through the OAuth dance if the client obtained a refresh token.
+ This refresh token and authentication credentials can be used to
+ obtain a new access token, and possibly a new refresh token.
+
+ :param token_url: Provider token refresh endpoint URL.
+
+ :param refresh_token: Refresh token string.
+
+ :param body: Request body (URL encoded string).
+
+ :param scope: List of scopes to request. Must be equal to
+ or a subset of the scopes granted when obtaining the refresh
+ token.
+
+ :param kwargs: Additional parameters to included in the request.
+
+ :returns: The prepared request tuple with (url, headers, body).
+ """
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ self.scope = scope or self.scope
+ body = self._client.prepare_refresh_body(body=body,
+ refresh_token=refresh_token, scope=self.scope, **kwargs)
+ return token_url, FORM_ENC_HEADERS, body
+
+ def prepare_token_revocation_request(self, revocation_url, token,
+ token_type_hint="access_token", body='', callback=None, **kwargs):
+ """Prepare a token revocation request.
+
+ :param revocation_url: Provider token revocation endpoint URL.
+
+ :param token: The access or refresh token to be revoked (string).
+
+ :param token_type_hint: ``"access_token"`` (default) or
+ ``"refresh_token"``. This is optional and if you wish to not pass it you
+ must provide ``token_type_hint=None``.
+
+ :param callback: A jsonp callback such as ``package.callback`` to be invoked
+ upon receiving the response. Not that it should not include a () suffix.
+
+ :param kwargs: Additional parameters to included in the request.
+
+ :returns: The prepared request tuple with (url, headers, body).
+
+ Note that JSONP request may use GET requests as the parameters will
+ be added to the request URL query as opposed to the request body.
+
+ An example of a revocation request
+
+ .. code-block: http
+
+ POST /revoke HTTP/1.1
+ Host: server.example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
+
+ token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
+
+ An example of a jsonp revocation request
+
+ .. code-block: http
+
+ GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
+ Host: server.example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
+
+ and an error response
+
+ .. code-block: http
+
+ package.myCallback({"error":"unsupported_token_type"});
+
+ Note that these requests usually require client credentials, client_id in
+ the case for public clients and provider specific authentication
+ credentials for confidential clients.
+ """
+ if not is_secure_transport(revocation_url):
+ raise InsecureTransportError()
+
+ return prepare_token_revocation_request(revocation_url, token,
+ token_type_hint=token_type_hint, body=body, callback=callback,
+ **kwargs)
+
+ def parse_request_body_response(self, body, scope=None, **kwargs):
+ """Parse the JSON response body.
+
+ If the access token request is valid and authorized, the
+ authorization server issues an access token as described in
+ `Section 5.1`_. A refresh token SHOULD NOT be included. If the request
+ failed client authentication or is invalid, the authorization server
+ returns an error response as described in `Section 5.2`_.
+
+ :param body: The response body from the token request.
+ :param scope: Scopes originally requested.
+ :return: Dictionary of token parameters.
+ :raises: Warning if scope has changed. OAuth2Error if response is invalid.
+
+ These response are json encoded and could easily be parsed without
+ the assistance of OAuthLib. However, there are a few subtle issues
+ to be aware of regarding the response which are helpfully addressed
+ through the raising of various errors.
+
+ A successful response should always contain
+
+ **access_token**
+ The access token issued by the authorization server. Often
+ a random string.
+
+ **token_type**
+ The type of the token issued as described in `Section 7.1`_.
+ Commonly ``Bearer``.
+
+ While it is not mandated it is recommended that the provider include
+
+ **expires_in**
+ The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ **scope**
+ Providers may supply this in all responses but are required to only
+ if it has changed since the authorization request.
+
+ .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ """
+ self.token = parse_token_response(body, scope=scope)
+ self._populate_attributes(self.token)
+ return self.token
+
+ def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
+ """Prepare an access token request, using a refresh token.
+
+ If the authorization server issued a refresh token to the client, the
+ client makes a refresh request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format in the HTTP request entity-body:
+
+ grant_type
+ REQUIRED. Value MUST be set to "refresh_token".
+ refresh_token
+ REQUIRED. The refresh token issued to the client.
+ scope
+ OPTIONAL. The scope of the access request as described by
+ Section 3.3. The requested scope MUST NOT include any scope
+ not originally granted by the resource owner, and if omitted is
+ treated as equal to the scope originally granted by the
+ resource owner.
+ """
+ refresh_token = refresh_token or self.refresh_token
+ return prepare_token_request('refresh_token', body=body, scope=scope,
+ refresh_token=refresh_token, **kwargs)
+
+ def _add_bearer_token(self, uri, http_method='GET', body=None,
+ headers=None, token_placement=None):
+ """Add a bearer token to the request uri, body or authorization header."""
+ if token_placement == AUTH_HEADER:
+ headers = tokens.prepare_bearer_headers(self.access_token, headers)
+
+ elif token_placement == URI_QUERY:
+ uri = tokens.prepare_bearer_uri(self.access_token, uri)
+
+ elif token_placement == BODY:
+ body = tokens.prepare_bearer_body(self.access_token, body)
+
+ else:
+ raise ValueError("Invalid token placement.")
+ return uri, headers, body
+
+ def _add_mac_token(self, uri, http_method='GET', body=None,
+ headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
+ """Add a MAC token to the request authorization header.
+
+ Warning: MAC token support is experimental as the spec is not yet stable.
+ """
+ headers = tokens.prepare_mac_header(self.access_token, uri,
+ self.mac_key, http_method, headers=headers, body=body, ext=ext,
+ hash_algorithm=self.mac_algorithm, **kwargs)
+ return uri, headers, body
+
+ def _populate_attributes(self, response):
+ """Add commonly used values such as access_token to self."""
+
+ if 'access_token' in response:
+ self.access_token = response.get('access_token')
+
+ if 'refresh_token' in response:
+ self.refresh_token = response.get('refresh_token')
+
+ if 'token_type' in response:
+ self.token_type = response.get('token_type')
+
+ if 'expires_in' in response:
+ self.expires_in = response.get('expires_in')
+ self._expires_at = time.time() + int(self.expires_in)
+
+ if 'expires_at' in response:
+ self._expires_at = int(response.get('expires_at'))
+
+ if 'code' in response:
+ self.code = response.get('code')
+
+ if 'mac_key' in response:
+ self.mac_key = response.get('mac_key')
+
+ if 'mac_algorithm' in response:
+ self.mac_algorithm = response.get('mac_algorithm')
+
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_token_request
+from ..parameters import parse_token_response
+
+
+class LegacyApplicationClient(Client):
+
+ """A public client using the resource owner password and username directly.
+
+ The resource owner password credentials grant type is suitable in
+ cases where the resource owner has a trust relationship with the
+ client, such as the device operating system or a highly privileged
+ application. The authorization server should take special care when
+ enabling this grant type, and only allow it when other flows are not
+ viable.
+
+ The grant type is suitable for clients capable of obtaining the
+ resource owner's credentials (username and password, typically using
+ an interactive form). It is also used to migrate existing clients
+ using direct authentication schemes such as HTTP Basic or Digest
+ authentication to OAuth by converting the stored credentials to an
+ access token.
+
+ The method through which the client obtains the resource owner
+ credentials is beyond the scope of this specification. The client
+ MUST discard the credentials once an access token has been obtained.
+ """
+
+ def __init__(self, client_id, **kwargs):
+ super(LegacyApplicationClient, self).__init__(client_id, **kwargs)
+
+ def prepare_request_body(self, username, password, body='', scope=None, **kwargs):
+ """Add the resource owner password and username to the request body.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format per `Appendix B`_ in the HTTP request entity-body:
+
+ :param username: The resource owner username.
+ :param password: The resource owner password.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+ :param kwargs: Extra credentials to include in the token request.
+
+ If the client type is confidential or the client was issued client
+ credentials (or assigned other authentication requirements), the
+ client MUST authenticate with the authorization server as described
+ in `Section 3.2.1`_.
+
+ The prepared body will include all provided credentials as well as
+ the ``grant_type`` parameter set to ``password``::
+
+ >>> from oauthlib.oauth2 import LegacyApplicationClient
+ >>> client = LegacyApplicationClient('your_id')
+ >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world'])
+ 'grant_type=password&username=foo&scope=hello+world&password=bar'
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ return prepare_token_request('password', body=body, username=username,
+ password=password, scope=scope, **kwargs)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_grant_uri
+from ..parameters import parse_implicit_response
+
+
+class MobileApplicationClient(Client):
+
+ """A public client utilizing the implicit code grant workflow.
+
+ A user-agent-based application is a public client in which the
+ client code is downloaded from a web server and executes within a
+ user-agent (e.g. web browser) on the device used by the resource
+ owner. Protocol data and credentials are easily accessible (and
+ often visible) to the resource owner. Since such applications
+ reside within the user-agent, they can make seamless use of the
+ user-agent capabilities when requesting authorization.
+
+ The implicit grant type is used to obtain access tokens (it does not
+ support the issuance of refresh tokens) and is optimized for public
+ clients known to operate a particular redirection URI. These clients
+ are typically implemented in a browser using a scripting language
+ such as JavaScript.
+
+ As a redirection-based flow, the client must be capable of
+ interacting with the resource owner's user-agent (typically a web
+ browser) and capable of receiving incoming requests (via redirection)
+ from the authorization server.
+
+ Unlike the authorization code grant type in which the client makes
+ separate requests for authorization and access token, the client
+ receives the access token as the result of the authorization request.
+
+ The implicit grant type does not include client authentication, and
+ relies on the presence of the resource owner and the registration of
+ the redirection URI. Because the access token is encoded into the
+ redirection URI, it may be exposed to the resource owner and other
+ applications residing on the same device.
+ """
+
+ def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
+ state=None, **kwargs):
+ """Prepare the implicit grant request URI.
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
+ and it should have been registerd with the OAuth
+ provider prior to use. As described in `Section 3.1.2`_.
+
+ :param scope: OPTIONAL. The scope of the access request as described by
+ Section 3.3`_. These may be any string but are commonly
+ URIs or various categories such as ``videos`` or ``documents``.
+
+ :param state: RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ :param kwargs: Extra arguments to include in the request URI.
+
+ In addition to supplied parameters, OAuthLib will append the ``client_id``
+ that was provided in the constructor as well as the mandatory ``response_type``
+ argument, set to ``token``::
+
+ >>> from oauthlib.oauth2 import MobileApplicationClient
+ >>> client = MobileApplicationClient('your_id')
+ >>> client.prepare_request_uri('https://example.com')
+ 'https://example.com?client_id=your_id&response_type=token'
+ >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
+ 'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
+ >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
+ 'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures'
+ >>> client.prepare_request_uri('https://example.com', foo='bar')
+ 'https://example.com?client_id=your_id&response_type=token&foo=bar'
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ return prepare_grant_uri(uri, self.client_id, 'token',
+ redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
+
+ def parse_request_uri_response(self, uri, state=None, scope=None):
+ """Parse the response URI fragment.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ the following parameters to the fragment component of the redirection
+ URI using the "application/x-www-form-urlencoded" format:
+
+ :param uri: The callback URI that resulted from the user being redirected
+ back from the provider to you, the client.
+ :param state: The state provided in the authorization request.
+ :param scope: The scopes provided in the authorization request.
+ :return: Dictionary of token parameters.
+ :raises: OAuth2Error if response is invalid.
+
+ A successful response should always contain
+
+ **access_token**
+ The access token issued by the authorization server. Often
+ a random string.
+
+ **token_type**
+ The type of the token issued as described in `Section 7.1`_.
+ Commonly ``Bearer``.
+
+ **state**
+ If you provided the state parameter in the authorization phase, then
+ the provider is required to include that exact state value in the
+ response.
+
+ While it is not mandated it is recommended that the provider include
+
+ **expires_in**
+ The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ **scope**
+ Providers may supply this in all responses but are required to only
+ if it has changed since the authorization request.
+
+ A few example responses can be seen below::
+
+ >>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world'
+ >>> from oauthlib.oauth2 import MobileApplicationClient
+ >>> client = MobileApplicationClient('your_id')
+ >>> client.parse_request_uri_response(response_uri)
+ {
+ 'access_token': 'sdlfkj452',
+ 'token_type': 'Bearer',
+ 'state': 'ss345asyht',
+ 'scope': [u'hello', u'world']
+ }
+ >>> client.parse_request_uri_response(response_uri, state='other')
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response
+ **scope**
+ File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response
+ raise ValueError("Mismatching or missing state in params.")
+ ValueError: Mismatching or missing state in params.
+ >>> def alert_scope_changed(message, old, new):
+ ... print(message, old, new)
+ ...
+ >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
+ >>> client.parse_request_body_response(response_body, scope=['other'])
+ ('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
+
+ .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ """
+ self.token = parse_implicit_response(uri, state=state, scope=scope)
+ self._populate_attributes(self.token)
+ return self.token
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import time
+
+from oauthlib.common import to_unicode
+
+from .base import Client
+from ..parameters import prepare_token_request
+from ..parameters import parse_token_response
+
+
+class ServiceApplicationClient(Client):
+ """A public client utilizing the JWT bearer grant.
+
+ JWT bearer tokes can be used to request an access token when a client
+ wishes to utilize an existing trust relationship, expressed through the
+ semantics of (and digital signature or keyed message digest calculated
+ over) the JWT, without a direct user approval step at the authorization
+ server.
+
+ This grant type does not involve an authorization step. It may be
+ used by both public and confidential clients.
+ """
+
+ grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
+
+ def __init__(self, client_id, private_key=None, subject=None, issuer=None,
+ audience=None, **kwargs):
+ """Initalize a JWT client with defaults for implicit use later.
+
+ :param client_id: Client identifier given by the OAuth provider upon
+ registration.
+
+ :param private_key: Private key used for signing and encrypting.
+ Must be given as a string.
+
+ :param subject: The principal that is the subject of the JWT, i.e.
+ which user is the token requested on behalf of.
+ For example, ``foo@example.com.
+
+ :param issuer: The JWT MUST contain an "iss" (issuer) claim that
+ contains a unique identifier for the entity that issued
+ the JWT. For example, ``your-client@provider.com``.
+
+ :param audience: A value identifying the authorization server as an
+ intended audience, e.g.
+ ``https://provider.com/oauth2/token``.
+
+ :param kwargs: Additional arguments to pass to base client, such as
+ state and token. See Client.__init__.__doc__ for
+ details.
+ """
+ super(ServiceApplicationClient, self).__init__(client_id, **kwargs)
+ self.private_key = private_key
+ self.subject = subject
+ self.issuer = issuer
+ self.audience = audience
+
+ def prepare_request_body(self,
+ private_key=None,
+ subject=None,
+ issuer=None,
+ audience=None,
+ expires_at=None,
+ issued_at=None,
+ extra_claims=None,
+ body='',
+ scope=None,
+ **kwargs):
+ """Create and add a JWT assertion to the request body.
+
+ :param private_key: Private key used for signing and encrypting.
+ Must be given as a string.
+
+ :param subject: (sub) The principal that is the subject of the JWT,
+ i.e. which user is the token requested on behalf of.
+ For example, ``foo@example.com.
+
+ :param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that
+ contains a unique identifier for the entity that issued
+ the JWT. For example, ``your-client@provider.com``.
+
+ :param audience: (aud) A value identifying the authorization server as an
+ intended audience, e.g.
+ ``https://provider.com/oauth2/token``.
+
+ :param expires_at: A unix expiration timestamp for the JWT. Defaults
+ to an hour from now, i.e. ``time.time() + 3600``.
+
+ :param issued_at: A unix timestamp of when the JWT was created.
+ Defaults to now, i.e. ``time.time()``.
+
+ :param not_before: A unix timestamp after which the JWT may be used.
+ Not included unless provided.
+
+ :param jwt_id: A unique JWT token identifier. Not included unless
+ provided.
+
+ :param extra_claims: A dict of additional claims to include in the JWT.
+
+ :param scope: The scope of the access request.
+
+ :param body: Request body (string) with extra parameters.
+
+ :param kwargs: Extra credentials to include in the token request.
+
+ The "scope" parameter may be used, as defined in the Assertion
+ Framework for OAuth 2.0 Client Authentication and Authorization Grants
+ [I-D.ietf-oauth-assertions] specification, to indicate the requested
+ scope.
+
+ Authentication of the client is optional, as described in
+ `Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the
+ "client_id" is only needed when a form of client authentication that
+ relies on the parameter is used.
+
+ The following non-normative example demonstrates an Access Token
+ Request with a JWT as an authorization grant (with extra line breaks
+ for display purposes only):
+
+ .. code-block: http
+
+ POST /token.oauth2 HTTP/1.1
+ Host: as.example.com
+ Content-Type: application/x-www-form-urlencoded
+
+ grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
+ &assertion=eyJhbGciOiJFUzI1NiJ9.
+ eyJpc3Mi[...omitted for brevity...].
+ J9l-ZhwP[...omitted for brevity...]
+
+ .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ import jwt
+
+ key = private_key or self.private_key
+ if not key:
+ raise ValueError('An encryption key must be supplied to make JWT'
+ ' token requests.')
+ claim = {
+ 'iss': issuer or self.issuer,
+ 'aud': audience or self.issuer,
+ 'sub': subject or self.issuer,
+ 'exp': int(expires_at or time.time() + 3600),
+ 'iat': int(issued_at or time.time()),
+ }
+
+ for attr in ('iss', 'aud', 'sub'):
+ if claim[attr] is None:
+ raise ValueError(
+ 'Claim must include %s but none was given.' % attr)
+
+ if 'not_before' in kwargs:
+ claim['nbf'] = kwargs.pop('not_before')
+
+ if 'jwt_id' in kwargs:
+ claim['jti'] = kwargs.pop('jwt_id')
+
+ claim.update(extra_claims or {})
+
+ assertion = jwt.encode(claim, key, 'RS256')
+ assertion = to_unicode(assertion)
+
+ return prepare_token_request(self.grant_type,
+ body=body,
+ assertion=assertion,
+ scope=scope,
+ **kwargs)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_grant_uri, prepare_token_request
+from ..parameters import parse_authorization_code_response
+from ..parameters import parse_token_response
+
+
+class WebApplicationClient(Client):
+
+ """A client utilizing the authorization code grant workflow.
+
+ A web application is a confidential client running on a web
+ server. Resource owners access the client via an HTML user
+ interface rendered in a user-agent on the device used by the
+ resource owner. The client credentials as well as any access
+ token issued to the client are stored on the web server and are
+ not exposed to or accessible by the resource owner.
+
+ The authorization code grant type is used to obtain both access
+ tokens and refresh tokens and is optimized for confidential clients.
+ As a redirection-based flow, the client must be capable of
+ interacting with the resource owner's user-agent (typically a web
+ browser) and capable of receiving incoming requests (via redirection)
+ from the authorization server.
+ """
+
+ def __init__(self, client_id, code=None, **kwargs):
+ super(WebApplicationClient, self).__init__(client_id, **kwargs)
+ self.code = code
+
+ def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
+ state=None, **kwargs):
+ """Prepare the authorization code request URI
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
+ and it should have been registerd with the OAuth
+ provider prior to use. As described in `Section 3.1.2`_.
+
+ :param scope: OPTIONAL. The scope of the access request as described by
+ Section 3.3`_. These may be any string but are commonly
+ URIs or various categories such as ``videos`` or ``documents``.
+
+ :param state: RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ :param kwargs: Extra arguments to include in the request URI.
+
+ In addition to supplied parameters, OAuthLib will append the ``client_id``
+ that was provided in the constructor as well as the mandatory ``response_type``
+ argument, set to ``code``::
+
+ >>> from oauthlib.oauth2 import WebApplicationClient
+ >>> client = WebApplicationClient('your_id')
+ >>> client.prepare_request_uri('https://example.com')
+ 'https://example.com?client_id=your_id&response_type=code'
+ >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
+ 'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
+ >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
+ 'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures'
+ >>> client.prepare_request_uri('https://example.com', foo='bar')
+ 'https://example.com?client_id=your_id&response_type=code&foo=bar'
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ return prepare_grant_uri(uri, self.client_id, 'code',
+ redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
+
+ def prepare_request_body(self, client_id=None, code=None, body='',
+ redirect_uri=None, **kwargs):
+ """Prepare the access token request body.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format in the HTTP request entity-body:
+
+ :param client_id: REQUIRED, if the client is not authenticating with the
+ authorization server as described in `Section 3.2.1`_.
+
+ :param code: REQUIRED. The authorization code received from the
+ authorization server.
+
+ :param redirect_uri: REQUIRED, if the "redirect_uri" parameter was included in the
+ authorization request as described in `Section 4.1.1`_, and their
+ values MUST be identical.
+
+ :param kwargs: Extra parameters to include in the token request.
+
+ In addition OAuthLib will add the ``grant_type`` parameter set to
+ ``authorization_code``.
+
+ If the client type is confidential or the client was issued client
+ credentials (or assigned other authentication requirements), the
+ client MUST authenticate with the authorization server as described
+ in `Section 3.2.1`_::
+
+ >>> from oauthlib.oauth2 import WebApplicationClient
+ >>> client = WebApplicationClient('your_id')
+ >>> client.prepare_request_body(code='sh35ksdf09sf')
+ 'grant_type=authorization_code&code=sh35ksdf09sf'
+ >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
+ 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
+
+ .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
+ .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ code = code or self.code
+ return prepare_token_request('authorization_code', code=code, body=body,
+ client_id=self.client_id, redirect_uri=redirect_uri, **kwargs)
+
+ def parse_request_uri_response(self, uri, state=None):
+ """Parse the URI query for code and state.
+
+ If the resource owner grants the access request, the authorization
+ server issues an authorization code and delivers it to the client by
+ adding the following parameters to the query component of the
+ redirection URI using the "application/x-www-form-urlencoded" format:
+
+ :param uri: The callback URI that resulted from the user being redirected
+ back from the provider to you, the client.
+ :param state: The state provided in the authorization request.
+
+ **code**
+ The authorization code generated by the authorization server.
+ The authorization code MUST expire shortly after it is issued
+ to mitigate the risk of leaks. A maximum authorization code
+ lifetime of 10 minutes is RECOMMENDED. The client MUST NOT
+ use the authorization code more than once. If an authorization
+ code is used more than once, the authorization server MUST deny
+ the request and SHOULD revoke (when possible) all tokens
+ previously issued based on that authorization code.
+ The authorization code is bound to the client identifier and
+ redirection URI.
+
+ **state**
+ If the "state" parameter was present in the authorization request.
+
+ This method is mainly intended to enforce strict state checking with
+ the added benefit of easily extracting parameters from the URI::
+
+ >>> from oauthlib.oauth2 import WebApplicationClient
+ >>> client = WebApplicationClient('your_id')
+ >>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45'
+ >>> client.parse_request_uri_response(uri, state='sfetw45')
+ {'state': 'sfetw45', 'code': 'sdfkjh345'}
+ >>> client.parse_request_uri_response(uri, state='other')
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response
+ back from the provider to you, the client.
+ File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response
+ raise MismatchingStateError()
+ oauthlib.oauth2.rfc6749.errors.MismatchingStateError
+ """
+ response = parse_authorization_code_response(uri, state=state)
+ self._populate_attributes(response)
+ return response
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .authorization import AuthorizationEndpoint
+from .token import TokenEndpoint
+from .resource import ResourceEndpoint
+from .revocation import RevocationEndpoint
+from .pre_configured import Server
+from .pre_configured import WebApplicationServer
+from .pre_configured import MobileApplicationServer
+from .pre_configured import LegacyApplicationServer
+from .pre_configured import BackendApplicationServer
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationEndpoint(BaseEndpoint):
+
+ """Authorization endpoint - used by the client to obtain authorization
+ from the resource owner via user-agent redirection.
+
+ The authorization endpoint is used to interact with the resource
+ owner and obtain an authorization grant. The authorization server
+ MUST first verify the identity of the resource owner. The way in
+ which the authorization server authenticates the resource owner (e.g.
+ username and password login, session cookies) is beyond the scope of
+ this specification.
+
+ The endpoint URI MAY include an "application/x-www-form-urlencoded"
+ formatted (per `Appendix B`_) query component,
+ which MUST be retained when adding additional query parameters. The
+ endpoint URI MUST NOT include a fragment component::
+
+ https://example.com/path?query=component # OK
+ https://example.com/path?query=component#fragment # Not OK
+
+ Since requests to the authorization endpoint result in user
+ authentication and the transmission of clear-text credentials (in the
+ HTTP response), the authorization server MUST require the use of TLS
+ as described in Section 1.6 when sending requests to the
+ authorization endpoint::
+
+ # We will deny any request which URI schema is not with https
+
+ The authorization server MUST support the use of the HTTP "GET"
+ method [RFC2616] for the authorization endpoint, and MAY support the
+ use of the "POST" method as well::
+
+ # HTTP method is currently not enforced
+
+ Parameters sent without a value MUST be treated as if they were
+ omitted from the request. The authorization server MUST ignore
+ unrecognized request parameters. Request and response parameters
+ MUST NOT be included more than once::
+
+ # Enforced through the design of oauthlib.common.Request
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ """
+
+ def __init__(self, default_response_type, default_token_type,
+ response_types):
+ BaseEndpoint.__init__(self)
+ self._response_types = response_types
+ self._default_response_type = default_response_type
+ self._default_token_type = default_token_type
+
+ @property
+ def response_types(self):
+ return self._response_types
+
+ @property
+ def default_response_type(self):
+ return self._default_response_type
+
+ @property
+ def default_response_type_handler(self):
+ return self.response_types.get(self.default_response_type)
+
+ @property
+ def default_token_type(self):
+ return self._default_token_type
+
+ @catch_errors_and_unavailability
+ def create_authorization_response(self, uri, http_method='GET', body=None,
+ headers=None, scopes=None, credentials=None):
+ """Extract response_type and route to the designated handler."""
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ request.scopes = scopes
+ # TODO: decide whether this should be a required argument
+ request.user = None # TODO: explain this in docs
+ for k, v in (credentials or {}).items():
+ setattr(request, k, v)
+ response_type_handler = self.response_types.get(
+ request.response_type, self.default_response_type_handler)
+ log.debug('Dispatching response_type %s request to %r.',
+ request.response_type, response_type_handler)
+ return response_type_handler.create_authorization_response(
+ request, self.default_token_type)
+
+ @catch_errors_and_unavailability
+ def validate_authorization_request(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Extract response_type and route to the designated handler."""
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ request.scopes = None
+ response_type_handler = self.response_types.get(
+ request.response_type, self.default_response_type_handler)
+ return response_type_handler.validate_authorization_request(request)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import functools
+import logging
+
+from ..errors import TemporarilyUnavailableError, ServerError
+from ..errors import FatalClientError, OAuth2Error
+
+log = logging.getLogger(__name__)
+
+
+class BaseEndpoint(object):
+
+ def __init__(self):
+ self._available = True
+ self._catch_errors = False
+
+ @property
+ def available(self):
+ return self._available
+
+ @available.setter
+ def available(self, available):
+ self._available = available
+
+ @property
+ def catch_errors(self):
+ return self._catch_errors
+
+ @catch_errors.setter
+ def catch_errors(self, catch_errors):
+ self._catch_errors = catch_errors
+
+
+def catch_errors_and_unavailability(f):
+ @functools.wraps(f)
+ def wrapper(endpoint, uri, *args, **kwargs):
+ if not endpoint.available:
+ e = TemporarilyUnavailableError()
+ log.info('Endpoint unavailable, ignoring request %s.' % uri)
+ return {}, e.json, 503
+
+ if endpoint.catch_errors:
+ try:
+ return f(endpoint, uri, *args, **kwargs)
+ except OAuth2Error:
+ raise
+ except FatalClientError:
+ raise
+ except Exception as e:
+ error = ServerError()
+ log.warning(
+ 'Exception caught while processing request, %s.' % e)
+ return {}, error.json, 500
+ else:
+ return f(endpoint, uri, *args, **kwargs)
+ return wrapper
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from ..tokens import BearerToken
+from ..grant_types import AuthorizationCodeGrant
+from ..grant_types import ImplicitGrant
+from ..grant_types import ResourceOwnerPasswordCredentialsGrant
+from ..grant_types import ClientCredentialsGrant
+from ..grant_types import RefreshTokenGrant
+
+from .authorization import AuthorizationEndpoint
+from .token import TokenEndpoint
+from .resource import ResourceEndpoint
+from .revocation import RevocationEndpoint
+
+
+class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
+ RevocationEndpoint):
+
+ """An all-in-one endpoint featuring all four major grant types."""
+
+ def __init__(self, request_validator, token_expires_in=None,
+ token_generator=None, refresh_token_generator=None,
+ *args, **kwargs):
+ """Construct a new all-grants-in-one server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ auth_grant = AuthorizationCodeGrant(request_validator)
+ implicit_grant = ImplicitGrant(request_validator)
+ password_grant = ResourceOwnerPasswordCredentialsGrant(
+ request_validator)
+ credentials_grant = ClientCredentialsGrant(request_validator)
+ refresh_grant = RefreshTokenGrant(request_validator)
+ bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ AuthorizationEndpoint.__init__(self, default_response_type='code',
+ response_types={
+ 'code': auth_grant,
+ 'token': implicit_grant,
+ },
+ default_token_type=bearer)
+ TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+ grant_types={
+ 'authorization_code': auth_grant,
+ 'password': password_grant,
+ 'client_credentials': credentials_grant,
+ 'refresh_token': refresh_grant,
+ },
+ default_token_type=bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': bearer})
+ RevocationEndpoint.__init__(self, request_validator)
+
+
+class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
+ RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a new web application server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ auth_grant = AuthorizationCodeGrant(request_validator)
+ refresh_grant = RefreshTokenGrant(request_validator)
+ bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ AuthorizationEndpoint.__init__(self, default_response_type='code',
+ response_types={'code': auth_grant},
+ default_token_type=bearer)
+ TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+ grant_types={
+ 'authorization_code': auth_grant,
+ 'refresh_token': refresh_grant,
+ },
+ default_token_type=bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': bearer})
+ RevocationEndpoint.__init__(self, request_validator)
+
+
+class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint,
+ RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a new implicit grant server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ implicit_grant = ImplicitGrant(request_validator)
+ bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ AuthorizationEndpoint.__init__(self, default_response_type='token',
+ response_types={
+ 'token': implicit_grant},
+ default_token_type=bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': bearer})
+ RevocationEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
+
+
+class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint,
+ RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a resource owner password credentials grant server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ password_grant = ResourceOwnerPasswordCredentialsGrant(
+ request_validator)
+ refresh_grant = RefreshTokenGrant(request_validator)
+ bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ TokenEndpoint.__init__(self, default_grant_type='password',
+ grant_types={
+ 'password': password_grant,
+ 'refresh_token': refresh_grant,
+ },
+ default_token_type=bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': bearer})
+ RevocationEndpoint.__init__(self, request_validator)
+
+
+class BackendApplicationServer(TokenEndpoint, ResourceEndpoint,
+ RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a client credentials grant server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ credentials_grant = ClientCredentialsGrant(request_validator)
+ bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ TokenEndpoint.__init__(self, default_grant_type='client_credentials',
+ grant_types={
+ 'client_credentials': credentials_grant},
+ default_token_type=bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': bearer})
+ RevocationEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class ResourceEndpoint(BaseEndpoint):
+
+ """Authorizes access to protected resources.
+
+ The client accesses protected resources by presenting the access
+ token to the resource server. The resource server MUST validate the
+ access token and ensure that it has not expired and that its scope
+ covers the requested resource. The methods used by the resource
+ server to validate the access token (as well as any error responses)
+ are beyond the scope of this specification but generally involve an
+ interaction or coordination between the resource server and the
+ authorization server::
+
+ # For most cases, returning a 403 should suffice.
+
+ The method in which the client utilizes the access token to
+ authenticate with the resource server depends on the type of access
+ token issued by the authorization server. Typically, it involves
+ using the HTTP "Authorization" request header field [RFC2617] with an
+ authentication scheme defined by the specification of the access
+ token type used, such as [RFC6750]::
+
+ # Access tokens may also be provided in query and body
+ https://example.com/protected?access_token=kjfch2345sdf # Query
+ access_token=sdf23409df # Body
+ """
+
+ def __init__(self, default_token, token_types):
+ BaseEndpoint.__init__(self)
+ self._tokens = token_types
+ self._default_token = default_token
+
+ @property
+ def default_token(self):
+ return self._default_token
+
+ @property
+ def default_token_type_handler(self):
+ return self.tokens.get(self.default_token)
+
+ @property
+ def tokens(self):
+ return self._tokens
+
+ @catch_errors_and_unavailability
+ def verify_request(self, uri, http_method='GET', body=None, headers=None,
+ scopes=None):
+ """Validate client, code etc, return body + headers"""
+ request = Request(uri, http_method, body, headers)
+ request.token_type = self.find_token_type(request)
+ request.scopes = scopes
+ token_type_handler = self.tokens.get(request.token_type,
+ self.default_token_type_handler)
+ log.debug('Dispatching token_type %s request to %r.',
+ request.token_type, token_type_handler)
+ return token_type_handler.validate_request(request), request
+
+ def find_token_type(self, request):
+ """Token type identification.
+
+ RFC 6749 does not provide a method for easily differentiating between
+ different token types during protected resource access. We estimate
+ the most likely token type (if any) by asking each known token type
+ to give an estimation based on the request.
+ """
+ estimates = sorted(((t.estimate_type(request), n)
+ for n, t in self.tokens.items()))
+ return estimates[0][1] if len(estimates) else None
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.endpoint.revocation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11).
+
+.. _`Token Revocation`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+from ..errors import InvalidClientError, UnsupportedTokenTypeError
+from ..errors import InvalidRequestError, OAuth2Error
+
+log = logging.getLogger(__name__)
+
+
+class RevocationEndpoint(BaseEndpoint):
+
+ """Token revocation endpoint.
+
+ Endpoint used by authenticated clients to revoke access and refresh tokens.
+ Commonly this will be part of the Authorization Endpoint.
+ """
+
+ valid_token_types = ('access_token', 'refresh_token')
+
+ def __init__(self, request_validator, supported_token_types=None,
+ enable_jsonp=False):
+ BaseEndpoint.__init__(self)
+ self.request_validator = request_validator
+ self.supported_token_types = (
+ supported_token_types or self.valid_token_types)
+ self.enable_jsonp = enable_jsonp
+
+ @catch_errors_and_unavailability
+ def create_revocation_response(self, uri, http_method='POST', body=None,
+ headers=None):
+ """Revoke supplied access or refresh token.
+
+
+ The authorization server responds with HTTP status code 200 if the
+ token has been revoked sucessfully or if the client submitted an
+ invalid token.
+
+ Note: invalid tokens do not cause an error response since the client
+ cannot handle such an error in a reasonable way. Moreover, the purpose
+ of the revocation request, invalidating the particular token, is
+ already achieved.
+
+ The content of the response body is ignored by the client as all
+ necessary information is conveyed in the response code.
+
+ An invalid token type hint value is ignored by the authorization server
+ and does not influence the revocation response.
+ """
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ try:
+ self.validate_revocation_request(request)
+ log.debug('Token revocation valid for %r.', request)
+ except OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ response_body = e.json
+ if self.enable_jsonp and request.callback:
+ response_body = '%s(%s);' % (request.callback, response_body)
+ return {}, response_body, e.status_code
+
+ self.request_validator.revoke_token(request.token,
+ request.token_type_hint, request)
+
+ response_body = None
+ if self.enable_jsonp and request.callback:
+ response_body = request.callback + '();'
+ return {}, response_body, 200
+
+ def validate_revocation_request(self, request):
+ """Ensure the request is valid.
+
+ The client constructs the request by including the following parameters
+ using the "application/x-www-form-urlencoded" format in the HTTP
+ request entity-body:
+
+ token (REQUIRED). The token that the client wants to get revoked.
+
+ token_type_hint (OPTIONAL). A hint about the type of the token
+ submitted for revocation. Clients MAY pass this parameter in order to
+ help the authorization server to optimize the token lookup. If the
+ server is unable to locate the token using the given hint, it MUST
+ extend its search accross all of its supported token types. An
+ authorization server MAY ignore this parameter, particularly if it is
+ able to detect the token type automatically. This specification
+ defines two such values:
+
+ * access_token: An Access Token as defined in [RFC6749],
+ `section 1.4`_
+
+ * refresh_token: A Refresh Token as defined in [RFC6749],
+ `section 1.5`_
+
+ Specific implementations, profiles, and extensions of this
+ specification MAY define other values for this parameter using
+ the registry defined in `Section 4.1.2`_.
+
+ The client also includes its authentication credentials as described in
+ `Section 2.3`_. of [`RFC6749`_].
+
+ .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`section 2.3`: http://tools.ietf.org/html/rfc6749#section-2.3
+ .. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
+ .. _`RFC6749`: http://tools.ietf.org/html/rfc6749
+ """
+ if not request.token:
+ raise InvalidRequestError(request=request,
+ description='Missing token parameter.')
+
+ if not self.request_validator.authenticate_client(request):
+ raise InvalidClientError(request=request)
+
+ if (request.token_type_hint and
+ request.token_type_hint in self.valid_token_types and
+ request.token_type_hint not in self.supported_token_types):
+ raise UnsupportedTokenTypeError(request=request)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+
+log = logging.getLogger(__name__)
+
+
+class TokenEndpoint(BaseEndpoint):
+
+ """Token issuing endpoint.
+
+ The token endpoint is used by the client to obtain an access token by
+ presenting its authorization grant or refresh token. The token
+ endpoint is used with every authorization grant except for the
+ implicit grant type (since an access token is issued directly).
+
+ The means through which the client obtains the location of the token
+ endpoint are beyond the scope of this specification, but the location
+ is typically provided in the service documentation.
+
+ The endpoint URI MAY include an "application/x-www-form-urlencoded"
+ formatted (per `Appendix B`_) query component,
+ which MUST be retained when adding additional query parameters. The
+ endpoint URI MUST NOT include a fragment component::
+
+ https://example.com/path?query=component # OK
+ https://example.com/path?query=component#fragment # Not OK
+
+ Since requests to the authorization endpoint result in user
+ Since requests to the token endpoint result in the transmission of
+ clear-text credentials (in the HTTP request and response), the
+ authorization server MUST require the use of TLS as described in
+ Section 1.6 when sending requests to the token endpoint::
+
+ # We will deny any request which URI schema is not with https
+
+ The client MUST use the HTTP "POST" method when making access token
+ requests::
+
+ # HTTP method is currently not enforced
+
+ Parameters sent without a value MUST be treated as if they were
+ omitted from the request. The authorization server MUST ignore
+ unrecognized request parameters. Request and response parameters
+ MUST NOT be included more than once::
+
+ # Delegated to each grant type.
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ """
+
+ def __init__(self, default_grant_type, default_token_type, grant_types):
+ BaseEndpoint.__init__(self)
+ self._grant_types = grant_types
+ self._default_token_type = default_token_type
+ self._default_grant_type = default_grant_type
+
+ @property
+ def grant_types(self):
+ return self._grant_types
+
+ @property
+ def default_grant_type(self):
+ return self._default_grant_type
+
+ @property
+ def default_grant_type_handler(self):
+ return self.grant_types.get(self.default_grant_type)
+
+ @property
+ def default_token_type(self):
+ return self._default_token_type
+
+ @catch_errors_and_unavailability
+ def create_token_response(self, uri, http_method='GET', body=None,
+ headers=None, credentials=None):
+ """Extract grant_type and route to the designated handler."""
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ request.scopes = None
+ request.extra_credentials = credentials
+ grant_type_handler = self.grant_types.get(request.grant_type,
+ self.default_grant_type_handler)
+ log.debug('Dispatching grant_type %s request to %r.',
+ request.grant_type, grant_type_handler)
+ return grant_type_handler.create_token_response(
+ request, self.default_token_type)
--- /dev/null
+# coding=utf-8
+"""
+oauthlib.oauth2.rfc6749.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 2 clients and provicers to represent the spec
+defined error responses for all four core grant types.
+"""
+from __future__ import unicode_literals
+import json
+from oauthlib.common import urlencode, add_params_to_uri
+
+
+class OAuth2Error(Exception):
+ error = None
+ status_code = 400
+ description = ''
+
+ def __init__(self, description=None, uri=None, state=None, status_code=None,
+ request=None):
+ """
+ description: A human-readable ASCII [USASCII] text providing
+ additional information, used to assist the client
+ developer in understanding the error that occurred.
+ Values for the "error_description" parameter MUST NOT
+ include characters outside the set
+ x20-21 / x23-5B / x5D-7E.
+
+ uri: A URI identifying a human-readable web page with information
+ about the error, used to provide the client developer with
+ additional information about the error. Values for the
+ "error_uri" parameter MUST conform to the URI- Reference
+ syntax, and thus MUST NOT include characters outside the set
+ x21 / x23-5B / x5D-7E.
+
+ state: A CSRF protection value received from the client.
+
+ request: Oauthlib Request object
+ """
+ self.description = description or self.description
+ message = '(%s) %s' % (self.error, self.description)
+ if request:
+ message += ' ' + repr(request)
+ super(OAuth2Error, self).__init__(message)
+
+ self.uri = uri
+ self.state = state
+
+ if status_code:
+ self.status_code = status_code
+
+ if request:
+ self.redirect_uri = request.redirect_uri
+ self.client_id = request.client_id
+ self.scopes = request.scopes
+ self.response_type = request.response_type
+ self.grant_type = request.grant_type
+ if not state:
+ self.state = request.state
+
+ def in_uri(self, uri):
+ return add_params_to_uri(uri, self.twotuples)
+
+ @property
+ def twotuples(self):
+ error = [('error', self.error)]
+ if self.description:
+ error.append(('error_description', self.description))
+ if self.uri:
+ error.append(('error_uri', self.uri))
+ if self.state:
+ error.append(('state', self.state))
+ return error
+
+ @property
+ def urlencoded(self):
+ return urlencode(self.twotuples)
+
+ @property
+ def json(self):
+ return json.dumps(dict(self.twotuples))
+
+
+class TokenExpiredError(OAuth2Error):
+ error = 'token_expired'
+
+
+class InsecureTransportError(OAuth2Error):
+ error = 'insecure_transport'
+ description = 'OAuth 2 MUST utilize https.'
+
+
+class MismatchingStateError(OAuth2Error):
+ error = 'mismatching_state'
+ description = 'CSRF Warning! State not equal in request and response.'
+
+
+class MissingCodeError(OAuth2Error):
+ error = 'missing_code'
+
+
+class MissingTokenError(OAuth2Error):
+ error = 'missing_token'
+
+
+class MissingTokenTypeError(OAuth2Error):
+ error = 'missing_token_type'
+
+
+class FatalClientError(OAuth2Error):
+
+ """Errors during authorization where user should not be redirected back.
+
+ If the request fails due to a missing, invalid, or mismatching
+ redirection URI, or if the client identifier is missing or invalid,
+ the authorization server SHOULD inform the resource owner of the
+ error and MUST NOT automatically redirect the user-agent to the
+ invalid redirection URI.
+
+ Instead the user should be informed of the error by the provider itself.
+ """
+ pass
+
+
+class InvalidRedirectURIError(FatalClientError):
+ error = 'invalid_redirect_uri'
+
+
+class MissingRedirectURIError(FatalClientError):
+ error = 'missing_redirect_uri'
+
+
+class MismatchingRedirectURIError(FatalClientError):
+ error = 'mismatching_redirect_uri'
+
+
+class MissingClientIdError(FatalClientError):
+ error = 'invalid_client_id'
+
+
+class InvalidClientIdError(FatalClientError):
+ error = 'invalid_client_id'
+
+
+class InvalidRequestError(OAuth2Error):
+
+ """The request is missing a required parameter, includes an invalid
+ parameter value, includes a parameter more than once, or is
+ otherwise malformed.
+ """
+ error = 'invalid_request'
+
+
+class AccessDeniedError(OAuth2Error):
+
+ """The resource owner or authorization server denied the request."""
+ error = 'access_denied'
+ status_code = 401
+
+
+class UnsupportedResponseTypeError(OAuth2Error):
+
+ """The authorization server does not support obtaining an authorization
+ code using this method.
+ """
+ error = 'unsupported_response_type'
+
+
+class InvalidScopeError(OAuth2Error):
+
+ """The requested scope is invalid, unknown, or malformed."""
+ error = 'invalid_scope'
+ status_code = 401
+
+
+class ServerError(OAuth2Error):
+
+ """The authorization server encountered an unexpected condition that
+ prevented it from fulfilling the request. (This error code is needed
+ because a 500 Internal Server Error HTTP status code cannot be returned
+ to the client via a HTTP redirect.)
+ """
+ error = 'server_error'
+
+
+class TemporarilyUnavailableError(OAuth2Error):
+
+ """The authorization server is currently unable to handle the request
+ due to a temporary overloading or maintenance of the server.
+ (This error code is needed because a 503 Service Unavailable HTTP
+ status code cannot be returned to the client via a HTTP redirect.)
+ """
+ error = 'temporarily_unavailable'
+
+
+class InvalidClientError(OAuth2Error):
+
+ """Client authentication failed (e.g. unknown client, no client
+ authentication included, or unsupported authentication method).
+ The authorization server MAY return an HTTP 401 (Unauthorized) status
+ code to indicate which HTTP authentication schemes are supported.
+ If the client attempted to authenticate via the "Authorization" request
+ header field, the authorization server MUST respond with an
+ HTTP 401 (Unauthorized) status code, and include the "WWW-Authenticate"
+ response header field matching the authentication scheme used by the
+ client.
+ """
+ error = 'invalid_client'
+ status_code = 401
+
+
+class InvalidGrantError(OAuth2Error):
+
+ """The provided authorization grant (e.g. authorization code, resource
+ owner credentials) or refresh token is invalid, expired, revoked, does
+ not match the redirection URI used in the authorization request, or was
+ issued to another client.
+ """
+ error = 'invalid_grant'
+ status_code = 401
+
+
+class UnauthorizedClientError(OAuth2Error):
+
+ """The authenticated client is not authorized to use this authorization
+ grant type.
+ """
+ error = 'unauthorized_client'
+ status_code = 401
+
+
+class UnsupportedGrantTypeError(OAuth2Error):
+
+ """The authorization grant type is not supported by the authorization
+ server.
+ """
+ error = 'unsupported_grant_type'
+
+
+class UnsupportedTokenTypeError(OAuth2Error):
+
+ """The authorization server does not support the revocation of the
+ presented token type. I.e. the client tried to revoke an access token
+ on a server not supporting this feature.
+ """
+ error = 'unsupported_token_type'
+
+
+def raise_from_error(error, params=None):
+ import inspect
+ import sys
+ kwargs = {
+ 'description': params.get('error_description'),
+ 'uri': params.get('error_uri'),
+ 'state': params.get('state')
+ }
+ for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
+ if cls.error == error:
+ raise cls(**kwargs)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+from .authorization_code import AuthorizationCodeGrant
+from .implicit import ImplicitGrant
+from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
+from .client_credentials import ClientCredentialsGrant
+from .refresh_token import RefreshTokenGrant
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from oauthlib import common
+from oauthlib.uri_validate import is_absolute_uri
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationCodeGrant(GrantTypeBase):
+
+ """`Authorization Code Grant`_
+
+ The authorization code grant type is used to obtain both access
+ tokens and refresh tokens and is optimized for confidential clients.
+ Since this is a redirection-based flow, the client must be capable of
+ interacting with the resource owner's user-agent (typically a web
+ browser) and capable of receiving incoming requests (via redirection)
+ from the authorization server::
+
+ +----------+
+ | Resource |
+ | Owner |
+ | |
+ +----------+
+ ^
+ |
+ (B)
+ +----|-----+ Client Identifier +---------------+
+ | -+----(A)-- & Redirection URI ---->| |
+ | User- | | Authorization |
+ | Agent -+----(B)-- User authenticates --->| Server |
+ | | | |
+ | -+----(C)-- Authorization Code ---<| |
+ +-|----|---+ +---------------+
+ | | ^ v
+ (A) (C) | |
+ | | | |
+ ^ v | |
+ +---------+ | |
+ | |>---(D)-- Authorization Code ---------' |
+ | Client | & Redirection URI |
+ | | |
+ | |<---(E)----- Access Token -------------------'
+ +---------+ (w/ Optional Refresh Token)
+
+ Note: The lines illustrating steps (A), (B), and (C) are broken into
+ two parts as they pass through the user-agent.
+
+ Figure 3: Authorization Code Flow
+
+ The flow illustrated in Figure 3 includes the following steps:
+
+ (A) The client initiates the flow by directing the resource owner's
+ user-agent to the authorization endpoint. The client includes
+ its client identifier, requested scope, local state, and a
+ redirection URI to which the authorization server will send the
+ user-agent back once access is granted (or denied).
+
+ (B) The authorization server authenticates the resource owner (via
+ the user-agent) and establishes whether the resource owner
+ grants or denies the client's access request.
+
+ (C) Assuming the resource owner grants access, the authorization
+ server redirects the user-agent back to the client using the
+ redirection URI provided earlier (in the request or during
+ client registration). The redirection URI includes an
+ authorization code and any local state provided by the client
+ earlier.
+
+ (D) The client requests an access token from the authorization
+ server's token endpoint by including the authorization code
+ received in the previous step. When making the request, the
+ client authenticates with the authorization server. The client
+ includes the redirection URI used to obtain the authorization
+ code for verification.
+
+ (E) The authorization server authenticates the client, validates the
+ authorization code, and ensures that the redirection URI
+ received matches the URI used to redirect the client in
+ step (C). If valid, the authorization server responds back with
+ an access token and, optionally, a refresh token.
+
+ .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1
+ """
+
+ def __init__(self, request_validator=None):
+ self.request_validator = request_validator or RequestValidator()
+
+ def create_authorization_code(self, request):
+ """Generates an authorization grant represented as a dictionary."""
+ grant = {'code': common.generate_token()}
+ if hasattr(request, 'state') and request.state:
+ grant['state'] = request.state
+ log.debug('Created authorization code grant %r for request %r.',
+ grant, request)
+ return grant
+
+ def create_authorization_response(self, request, token_handler):
+ """
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ response_type
+ REQUIRED. Value MUST be set to "code".
+ client_id
+ REQUIRED. The client identifier as described in `Section 2.2`_.
+ redirect_uri
+ OPTIONAL. As described in `Section 3.1.2`_.
+ scope
+ OPTIONAL. The scope of the access request as described by
+ `Section 3.3`_.
+ state
+ RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ The client directs the resource owner to the constructed URI using an
+ HTTP redirection response, or by other means available to it via the
+ user-agent.
+
+ :param request: oauthlib.commong.Request
+ :param token_handler: A token handler instace, for example of type
+ oauthlib.oauth2.BearerToken.
+ :returns: headers, body, status
+ :raises: FatalClientError on invalid redirect URI or client id.
+ ValueError if scopes are not set on the request object.
+
+ A few examples::
+
+ >>> from your_validator import your_validator
+ >>> request = Request('https://example.com/authorize?client_id=valid'
+ ... '&redirect_uri=http%3A%2F%2Fclient.com%2F')
+ >>> from oauthlib.common import Request
+ >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
+ >>> token = BearerToken(your_validator)
+ >>> grant = AuthorizationCodeGrant(your_validator)
+ >>> grant.create_authorization_response(request, token)
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response
+ raise ValueError('Scopes must be set on post auth.')
+ ValueError: Scopes must be set on post auth.
+ >>> request.scopes = ['authorized', 'in', 'some', 'form']
+ >>> grant.create_authorization_response(request, token)
+ (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
+ >>> request = Request('https://example.com/authorize?client_id=valid'
+ ... '&redirect_uri=http%3A%2F%2Fclient.com%2F'
+ ... '&response_type=code')
+ >>> request.scopes = ['authorized', 'in', 'some', 'form']
+ >>> grant.create_authorization_response(request, token)
+ (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
+ >>> # If the client id or redirect uri fails validation
+ >>> grant.create_authorization_response(request, token)
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
+ >>> grant.create_authorization_response(request, token)
+ File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
+ oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ try:
+ # request.scopes is only mandated in post auth and both pre and
+ # post auth use validate_authorization_request
+ if not request.scopes:
+ raise ValueError('Scopes must be set on post auth.')
+
+ self.validate_authorization_request(request)
+ log.debug('Pre resource owner authorization validation ok for %r.',
+ request)
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+ except errors.FatalClientError as e:
+ log.debug('Fatal client error during validation of %r. %r.',
+ request, e)
+ raise
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the query component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B:
+ # http://tools.ietf.org/html/rfc6749#appendix-B
+ except errors.OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ request.redirect_uri = request.redirect_uri or self.error_uri
+ return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples)}, None, 302
+
+ grant = self.create_authorization_code(request)
+ log.debug('Saving grant %r for %r.', grant, request)
+ self.request_validator.save_authorization_code(
+ request.client_id, grant, request)
+ return {'Location': common.add_params_to_uri(request.redirect_uri, grant.items())}, None, 302
+
+ def create_token_response(self, request, token_handler):
+ """Validate the authorization code.
+
+ The client MUST NOT use the authorization code more than once. If an
+ authorization code is used more than once, the authorization server
+ MUST deny the request and SHOULD revoke (when possible) all tokens
+ previously issued based on that authorization code. The authorization
+ code is bound to the client identifier and redirection URI.
+ """
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ try:
+ self.validate_token_request(request)
+ log.debug('Token request validation ok for %r.', request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request, refresh_token=True)
+ self.request_validator.invalidate_authorization_code(
+ request.client_id, request.code, request)
+ return headers, json.dumps(token), 200
+
+ def validate_authorization_request(self, request):
+ """Check the authorization request for normal and fatal errors.
+
+ A normal error could be a missing response_type parameter or the client
+ attempting to access scope it is not allowed to ask authorization for.
+ Normal errors can safely be included in the redirection URI and
+ sent back to the client.
+
+ Fatal errors occur when the client_id or redirect_uri is invalid or
+ missing. These must be caught by the provider and handled, how this
+ is done is outside of the scope of OAuthLib but showing an error
+ page describing the issue is a good idea.
+ """
+
+ # First check for fatal errors
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+
+ # REQUIRED. The client identifier as described in Section 2.2.
+ # http://tools.ietf.org/html/rfc6749#section-2.2
+ if not request.client_id:
+ raise errors.MissingClientIdError(request=request)
+
+ if not self.request_validator.validate_client_id(request.client_id, request):
+ raise errors.InvalidClientIdError(request=request)
+
+ # OPTIONAL. As described in Section 3.1.2.
+ # http://tools.ietf.org/html/rfc6749#section-3.1.2
+ log.debug('Validating redirection uri %s for client %s.',
+ request.redirect_uri, request.client_id)
+ if request.redirect_uri is not None:
+ request.using_default_redirect_uri = False
+ log.debug('Using provided redirect_uri %s', request.redirect_uri)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
+
+ if not self.request_validator.validate_redirect_uri(
+ request.client_id, request.redirect_uri, request):
+ raise errors.MismatchingRedirectURIError(request=request)
+ else:
+ request.redirect_uri = self.request_validator.get_default_redirect_uri(
+ request.client_id, request)
+ request.using_default_redirect_uri = True
+ log.debug('Using default redirect_uri %s.', request.redirect_uri)
+ if not request.redirect_uri:
+ raise errors.MissingRedirectURIError(request=request)
+
+ # Then check for normal errors.
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the query component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B.
+ # http://tools.ietf.org/html/rfc6749#appendix-B
+
+ # Note that the correct parameters to be added are automatically
+ # populated through the use of specific exceptions.
+ if request.response_type is None:
+ raise errors.InvalidRequestError(description='Missing response_type parameter.', request=request)
+
+ for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+ if not self.request_validator.validate_response_type(request.client_id,
+ request.response_type, request.client, request):
+ log.debug('Client %s is not authorized to use response_type %s.',
+ request.client_id, request.response_type)
+ raise errors.UnauthorizedClientError(request=request)
+
+ # REQUIRED. Value MUST be set to "code".
+ if request.response_type != 'code':
+ raise errors.UnsupportedResponseTypeError(request=request)
+
+ # OPTIONAL. The scope of the access request as described by Section 3.3
+ # http://tools.ietf.org/html/rfc6749#section-3.3
+ self.validate_scopes(request)
+
+ return request.scopes, {
+ 'client_id': request.client_id,
+ 'redirect_uri': request.redirect_uri,
+ 'response_type': request.response_type,
+ 'state': request.state,
+ 'request': request,
+ }
+
+ def validate_token_request(self, request):
+ # REQUIRED. Value MUST be set to "authorization_code".
+ if request.grant_type != 'authorization_code':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ if request.code is None:
+ raise errors.InvalidRequestError(
+ description='Missing code parameter.', request=request)
+
+ for param in ('client_id', 'grant_type', 'redirect_uri'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
+ request=request)
+
+ if self.request_validator.client_authentication_required(request):
+ # If the client type is confidential or the client was issued client
+ # credentials (or assigned other authentication requirements), the
+ # client MUST authenticate with the authorization server as described
+ # in Section 3.2.1.
+ # http://tools.ietf.org/html/rfc6749#section-3.2.1
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ # REQUIRED, if the client is not authenticating with the
+ # authorization server as described in Section 3.2.1.
+ # http://tools.ietf.org/html/rfc6749#section-3.2.1
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+
+ if not hasattr(request.client, 'client_id'):
+ raise NotImplementedError('Authenticate client must set the '
+ 'request.client.client_id attribute '
+ 'in authenticate_client.')
+
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ # REQUIRED. The authorization code received from the
+ # authorization server.
+ if not self.request_validator.validate_code(request.client_id,
+ request.code, request.client, request):
+ log.debug('Client, %r (%r), is not allowed access to scopes %r.',
+ request.client_id, request.client, request.scopes)
+ raise errors.InvalidGrantError(request=request)
+
+ for attr in ('user', 'state', 'scopes'):
+ if getattr(request, attr) is None:
+ log.debug('request.%s was not set on code validation.', attr)
+
+ # REQUIRED, if the "redirect_uri" parameter was included in the
+ # authorization request as described in Section 4.1.1, and their
+ # values MUST be identical.
+ if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
+ request.redirect_uri, request.client):
+ log.debug('Redirect_uri (%r) invalid for client %r (%r).',
+ request.redirect_uri, request.client_id, request.client)
+ raise errors.AccessDeniedError(request=request)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+from oauthlib.oauth2.rfc6749 import errors, utils
+
+log = logging.getLogger(__name__)
+
+
+class GrantTypeBase(object):
+ error_uri = None
+ request_validator = None
+
+ def create_authorization_response(self, request, token_handler):
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def create_token_response(self, request, token_handler):
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_grant_type(self, request):
+ if not self.request_validator.validate_grant_type(request.client_id,
+ request.grant_type, request.client, request):
+ log.debug('Unauthorized from %r (%r) access to grant type %s.',
+ request.client_id, request.client, request.grant_type)
+ raise errors.UnauthorizedClientError(request=request)
+
+ def validate_scopes(self, request):
+ if not request.scopes:
+ request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list(
+ self.request_validator.get_default_scopes(request.client_id, request))
+ log.debug('Validating access to scopes %r for client %r (%r).',
+ request.scopes, request.client_id, request.client)
+ if not self.request_validator.validate_scopes(request.client_id,
+ request.scopes, request.client, request):
+ raise errors.InvalidScopeError(request=request)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class ClientCredentialsGrant(GrantTypeBase):
+
+ """`Client Credentials Grant`_
+
+ The client can request an access token using only its client
+ credentials (or other supported means of authentication) when the
+ client is requesting access to the protected resources under its
+ control, or those of another resource owner that have been previously
+ arranged with the authorization server (the method of which is beyond
+ the scope of this specification).
+
+ The client credentials grant type MUST only be used by confidential
+ clients::
+
+ +---------+ +---------------+
+ : : : :
+ : :>-- A - Client Authentication --->: Authorization :
+ : Client : : Server :
+ : :<-- B ---- Access Token ---------<: :
+ : : : :
+ +---------+ +---------------+
+
+ Figure 6: Client Credentials Flow
+
+ The flow illustrated in Figure 6 includes the following steps:
+
+ (A) The client authenticates with the authorization server and
+ requests an access token from the token endpoint.
+
+ (B) The authorization server authenticates the client, and if valid,
+ issues an access token.
+
+ .. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4
+ """
+
+ def __init__(self, request_validator=None):
+ self.request_validator = request_validator or RequestValidator()
+
+ def create_token_response(self, request, token_handler):
+ """Return token or error in JSON format.
+
+ If the access token request is valid and authorized, the
+ authorization server issues an access token as described in
+ `Section 5.1`_. A refresh token SHOULD NOT be included. If the request
+ failed client authentication or is invalid, the authorization server
+ returns an error response as described in `Section 5.2`_.
+
+ .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ try:
+ log.debug('Validating access token request, %r.', request)
+ self.validate_token_request(request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error in token request. %s.', e)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request, refresh_token=False)
+ log.debug('Issuing token to client id %r (%r), %r.',
+ request.client_id, request.client, token)
+ return headers, json.dumps(token), 200
+
+ def validate_token_request(self, request):
+ if not getattr(request, 'grant_type'):
+ raise errors.InvalidRequestError('Request is missing grant type.',
+ request=request)
+
+ if not request.grant_type == 'client_credentials':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ for param in ('grant_type', 'scope'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
+ request=request)
+
+ log.debug('Authenticating client, %r.', request)
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ else:
+ if not hasattr(request.client, 'client_id'):
+ raise NotImplementedError('Authenticate client must set the '
+ 'request.client.client_id attribute '
+ 'in authenticate_client.')
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ log.debug('Authorizing access to user %r.', request.user)
+ request.client_id = request.client_id or request.client.client_id
+ self.validate_scopes(request)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+from oauthlib import common
+from oauthlib.uri_validate import is_absolute_uri
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class ImplicitGrant(GrantTypeBase):
+
+ """`Implicit Grant`_
+
+ The implicit grant type is used to obtain access tokens (it does not
+ support the issuance of refresh tokens) and is optimized for public
+ clients known to operate a particular redirection URI. These clients
+ are typically implemented in a browser using a scripting language
+ such as JavaScript.
+
+ Unlike the authorization code grant type, in which the client makes
+ separate requests for authorization and for an access token, the
+ client receives the access token as the result of the authorization
+ request.
+
+ The implicit grant type does not include client authentication, and
+ relies on the presence of the resource owner and the registration of
+ the redirection URI. Because the access token is encoded into the
+ redirection URI, it may be exposed to the resource owner and other
+ applications residing on the same device::
+
+ +----------+
+ | Resource |
+ | Owner |
+ | |
+ +----------+
+ ^
+ |
+ (B)
+ +----|-----+ Client Identifier +---------------+
+ | -+----(A)-- & Redirection URI --->| |
+ | User- | | Authorization |
+ | Agent -|----(B)-- User authenticates -->| Server |
+ | | | |
+ | |<---(C)--- Redirection URI ----<| |
+ | | with Access Token +---------------+
+ | | in Fragment
+ | | +---------------+
+ | |----(D)--- Redirection URI ---->| Web-Hosted |
+ | | without Fragment | Client |
+ | | | Resource |
+ | (F) |<---(E)------- Script ---------<| |
+ | | +---------------+
+ +-|--------+
+ | |
+ (A) (G) Access Token
+ | |
+ ^ v
+ +---------+
+ | |
+ | Client |
+ | |
+ +---------+
+
+ Note: The lines illustrating steps (A) and (B) are broken into two
+ parts as they pass through the user-agent.
+
+ Figure 4: Implicit Grant Flow
+
+ The flow illustrated in Figure 4 includes the following steps:
+
+ (A) The client initiates the flow by directing the resource owner's
+ user-agent to the authorization endpoint. The client includes
+ its client identifier, requested scope, local state, and a
+ redirection URI to which the authorization server will send the
+ user-agent back once access is granted (or denied).
+
+ (B) The authorization server authenticates the resource owner (via
+ the user-agent) and establishes whether the resource owner
+ grants or denies the client's access request.
+
+ (C) Assuming the resource owner grants access, the authorization
+ server redirects the user-agent back to the client using the
+ redirection URI provided earlier. The redirection URI includes
+ the access token in the URI fragment.
+
+ (D) The user-agent follows the redirection instructions by making a
+ request to the web-hosted client resource (which does not
+ include the fragment per [RFC2616]). The user-agent retains the
+ fragment information locally.
+
+ (E) The web-hosted client resource returns a web page (typically an
+ HTML document with an embedded script) capable of accessing the
+ full redirection URI including the fragment retained by the
+ user-agent, and extracting the access token (and other
+ parameters) contained in the fragment.
+
+ (F) The user-agent executes the script provided by the web-hosted
+ client resource locally, which extracts the access token.
+
+ (G) The user-agent passes the access token to the client.
+
+ See `Section 10.3`_ and `Section 10.16`_ for important security considerations
+ when using the implicit grant.
+
+ .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2
+ .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3
+ .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16
+ """
+
+ def __init__(self, request_validator=None):
+ self.request_validator = request_validator or RequestValidator()
+
+ def create_authorization_response(self, request, token_handler):
+ """Create an authorization response.
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ response_type
+ REQUIRED. Value MUST be set to "token".
+
+ client_id
+ REQUIRED. The client identifier as described in `Section 2.2`_.
+
+ redirect_uri
+ OPTIONAL. As described in `Section 3.1.2`_.
+
+ scope
+ OPTIONAL. The scope of the access request as described by
+ `Section 3.3`_.
+
+ state
+ RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ The authorization server validates the request to ensure that all
+ required parameters are present and valid. The authorization server
+ MUST verify that the redirection URI to which it will redirect the
+ access token matches a redirection URI registered by the client as
+ described in `Section 3.1.2`_.
+
+ .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ """
+ return self.create_token_response(request, token_handler)
+
+ def create_token_response(self, request, token_handler):
+ """Return token or error embedded in the URI fragment.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ the following parameters to the fragment component of the redirection
+ URI using the "application/x-www-form-urlencoded" format, per
+ `Appendix B`_:
+
+ access_token
+ REQUIRED. The access token issued by the authorization server.
+
+ token_type
+ REQUIRED. The type of the token issued as described in
+ `Section 7.1`_. Value is case insensitive.
+
+ expires_in
+ RECOMMENDED. The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ scope
+ OPTIONAL, if identical to the scope requested by the client;
+ otherwise, REQUIRED. The scope of the access token as
+ described by `Section 3.3`_.
+
+ state
+ REQUIRED if the "state" parameter was present in the client
+ authorization request. The exact value received from the
+ client.
+
+ The authorization server MUST NOT issue a refresh token.
+
+ .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ """
+ try:
+ # request.scopes is only mandated in post auth and both pre and
+ # post auth use validate_authorization_request
+ if not request.scopes:
+ raise ValueError('Scopes must be set on post auth.')
+
+ self.validate_token_request(request)
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+ except errors.FatalClientError as e:
+ log.debug('Fatal client error during validation of %r. %r.',
+ request, e)
+ raise
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the fragment component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B:
+ # http://tools.ietf.org/html/rfc6749#appendix-B
+ except errors.OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
+ fragment=True)}, None, 302
+
+ token = token_handler.create_token(request, refresh_token=False)
+ return {'Location': common.add_params_to_uri(request.redirect_uri, token.items(),
+ fragment=True)}, None, 302
+
+ def validate_authorization_request(self, request):
+ return self.validate_token_request(request)
+
+ def validate_token_request(self, request):
+ """Check the token request for normal and fatal errors.
+
+ This method is very similar to validate_authorization_request in
+ the AuthorizationCodeGrant but differ in a few subtle areas.
+
+ A normal error could be a missing response_type parameter or the client
+ attempting to access scope it is not allowed to ask authorization for.
+ Normal errors can safely be included in the redirection URI and
+ sent back to the client.
+
+ Fatal errors occur when the client_id or redirect_uri is invalid or
+ missing. These must be caught by the provider and handled, how this
+ is done is outside of the scope of OAuthLib but showing an error
+ page describing the issue is a good idea.
+ """
+
+ # First check for fatal errors
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+
+ # REQUIRED. The client identifier as described in Section 2.2.
+ # http://tools.ietf.org/html/rfc6749#section-2.2
+ if not request.client_id:
+ raise errors.MissingClientIdError(request=request)
+
+ if not self.request_validator.validate_client_id(request.client_id, request):
+ raise errors.InvalidClientIdError(request=request)
+
+ # OPTIONAL. As described in Section 3.1.2.
+ # http://tools.ietf.org/html/rfc6749#section-3.1.2
+ if request.redirect_uri is not None:
+ request.using_default_redirect_uri = False
+ log.debug('Using provided redirect_uri %s', request.redirect_uri)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
+
+ # The authorization server MUST verify that the redirection URI
+ # to which it will redirect the access token matches a
+ # redirection URI registered by the client as described in
+ # Section 3.1.2.
+ # http://tools.ietf.org/html/rfc6749#section-3.1.2
+ if not self.request_validator.validate_redirect_uri(
+ request.client_id, request.redirect_uri, request):
+ raise errors.MismatchingRedirectURIError(request=request)
+ else:
+ request.redirect_uri = self.request_validator.get_default_redirect_uri(
+ request.client_id, request)
+ request.using_default_redirect_uri = True
+ log.debug('Using default redirect_uri %s.', request.redirect_uri)
+ if not request.redirect_uri:
+ raise errors.MissingRedirectURIError(request=request)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
+
+ # Then check for normal errors.
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the fragment component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B.
+ # http://tools.ietf.org/html/rfc6749#appendix-B
+
+ # Note that the correct parameters to be added are automatically
+ # populated through the use of specific exceptions.
+ if request.response_type is None:
+ raise errors.InvalidRequestError(description='Missing response_type parameter.',
+ request=request)
+
+ for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+ # REQUIRED. Value MUST be set to "token".
+ if request.response_type != 'token':
+ raise errors.UnsupportedResponseTypeError(request=request)
+
+ log.debug('Validating use of response_type token for client %r (%r).',
+ request.client_id, request.client)
+ if not self.request_validator.validate_response_type(request.client_id,
+ request.response_type, request.client, request):
+ log.debug('Client %s is not authorized to use response_type %s.',
+ request.client_id, request.response_type)
+ raise errors.UnauthorizedClientError(request=request)
+
+ # OPTIONAL. The scope of the access request as described by Section 3.3
+ # http://tools.ietf.org/html/rfc6749#section-3.3
+ self.validate_scopes(request)
+
+ return request.scopes, {
+ 'client_id': request.client_id,
+ 'redirect_uri': request.redirect_uri,
+ 'response_type': request.response_type,
+ 'state': request.state,
+ 'request': request,
+ }
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from .base import GrantTypeBase
+from .. import errors, utils
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class RefreshTokenGrant(GrantTypeBase):
+
+ """`Refresh token grant`_
+
+ .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6
+ """
+
+ @property
+ def issue_new_refresh_tokens(self):
+ return True
+
+ def __init__(self, request_validator=None, issue_new_refresh_tokens=True):
+ self.request_validator = request_validator or RequestValidator()
+
+ def create_token_response(self, request, token_handler):
+ """Create a new access token from a refresh_token.
+
+ If valid and authorized, the authorization server issues an access
+ token as described in `Section 5.1`_. If the request failed
+ verification or is invalid, the authorization server returns an error
+ response as described in `Section 5.2`_.
+
+ The authorization server MAY issue a new refresh token, in which case
+ the client MUST discard the old refresh token and replace it with the
+ new refresh token. The authorization server MAY revoke the old
+ refresh token after issuing a new refresh token to the client. If a
+ new refresh token is issued, the refresh token scope MUST be
+ identical to that of the refresh token included by the client in the
+ request.
+
+ .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ try:
+ log.debug('Validating refresh token request, %r.', request)
+ self.validate_token_request(request)
+ except errors.OAuth2Error as e:
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request,
+ refresh_token=self.issue_new_refresh_tokens)
+ log.debug('Issuing new token to client id %r (%r), %r.',
+ request.client_id, request.client, token)
+ return headers, json.dumps(token), 200
+
+ def validate_token_request(self, request):
+ # REQUIRED. Value MUST be set to "refresh_token".
+ if request.grant_type != 'refresh_token':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ if request.refresh_token is None:
+ raise errors.InvalidRequestError(
+ description='Missing refresh token parameter.',
+ request=request)
+
+ # Because refresh tokens are typically long-lasting credentials used to
+ # request additional access tokens, the refresh token is bound to the
+ # client to which it was issued. If the client type is confidential or
+ # the client was issued client credentials (or assigned other
+ # authentication requirements), the client MUST authenticate with the
+ # authorization server as described in Section 3.2.1.
+ # http://tools.ietf.org/html/rfc6749#section-3.2.1
+ if self.request_validator.client_authentication_required(request):
+ log.debug('Authenticating client, %r.', request)
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Invalid client (%r), denying access.', request)
+ raise errors.InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ # REQUIRED. The refresh token issued to the client.
+ log.debug('Validating refresh token %s for client %r.',
+ request.refresh_token, request.client)
+ if not self.request_validator.validate_refresh_token(
+ request.refresh_token, request.client, request):
+ log.debug('Invalid refresh token, %s, for client %r.',
+ request.refresh_token, request.client)
+ raise errors.InvalidGrantError(request=request)
+
+ original_scopes = utils.scope_to_list(
+ self.request_validator.get_original_scopes(
+ request.refresh_token, request))
+
+ if request.scope:
+ request.scopes = utils.scope_to_list(request.scope)
+ if (not all((s in original_scopes for s in request.scopes))
+ and not self.request_validator.is_within_original_scope(
+ request.scopes, request.refresh_token, request)):
+ log.debug('Refresh token %s lack requested scopes, %r.',
+ request.refresh_token, request.scopes)
+ raise errors.InvalidScopeError(request=request)
+ else:
+ request.scopes = original_scopes
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
+
+ """`Resource Owner Password Credentials Grant`_
+
+ The resource owner password credentials grant type is suitable in
+ cases where the resource owner has a trust relationship with the
+ client, such as the device operating system or a highly privileged
+ application. The authorization server should take special care when
+ enabling this grant type and only allow it when other flows are not
+ viable.
+
+ This grant type is suitable for clients capable of obtaining the
+ resource owner's credentials (username and password, typically using
+ an interactive form). It is also used to migrate existing clients
+ using direct authentication schemes such as HTTP Basic or Digest
+ authentication to OAuth by converting the stored credentials to an
+ access token::
+
+ +----------+
+ | Resource |
+ | Owner |
+ | |
+ +----------+
+ v
+ | Resource Owner
+ (A) Password Credentials
+ |
+ v
+ +---------+ +---------------+
+ | |>--(B)---- Resource Owner ------->| |
+ | | Password Credentials | Authorization |
+ | Client | | Server |
+ | |<--(C)---- Access Token ---------<| |
+ | | (w/ Optional Refresh Token) | |
+ +---------+ +---------------+
+
+ Figure 5: Resource Owner Password Credentials Flow
+
+ The flow illustrated in Figure 5 includes the following steps:
+
+ (A) The resource owner provides the client with its username and
+ password.
+
+ (B) The client requests an access token from the authorization
+ server's token endpoint by including the credentials received
+ from the resource owner. When making the request, the client
+ authenticates with the authorization server.
+
+ (C) The authorization server authenticates the client and validates
+ the resource owner credentials, and if valid, issues an access
+ token.
+
+ .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3
+ """
+
+ def __init__(self, request_validator=None, refresh_token=True):
+ """
+ If the refresh_token keyword argument is False, do not return
+ a refresh token in the response.
+ """
+ self.request_validator = request_validator or RequestValidator()
+ self.refresh_token = refresh_token
+
+ def create_token_response(self, request, token_handler):
+ """Return token or error in json format.
+
+ If the access token request is valid and authorized, the
+ authorization server issues an access token and optional refresh
+ token as described in `Section 5.1`_. If the request failed client
+ authentication or is invalid, the authorization server returns an
+ error response as described in `Section 5.2`_.
+
+ .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ try:
+ if self.request_validator.client_authentication_required(request):
+ log.debug('Authenticating client, %r.', request)
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ log.debug('Validating access token request, %r.', request)
+ self.validate_token_request(request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error in token request, %s.', e)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request, self.refresh_token)
+ log.debug('Issuing token %r to client id %r (%r) and username %s.',
+ token, request.client_id, request.client, request.username)
+ return headers, json.dumps(token), 200
+
+ def validate_token_request(self, request):
+ """
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format per Appendix B with a character encoding of UTF-8 in the HTTP
+ request entity-body:
+
+ grant_type
+ REQUIRED. Value MUST be set to "password".
+
+ username
+ REQUIRED. The resource owner username.
+
+ password
+ REQUIRED. The resource owner password.
+
+ scope
+ OPTIONAL. The scope of the access request as described by
+ `Section 3.3`_.
+
+ If the client type is confidential or the client was issued client
+ credentials (or assigned other authentication requirements), the
+ client MUST authenticate with the authorization server as described
+ in `Section 3.2.1`_.
+
+ The authorization server MUST:
+
+ o require client authentication for confidential clients or for any
+ client that was issued client credentials (or with other
+ authentication requirements),
+
+ o authenticate the client if client authentication is included, and
+
+ o validate the resource owner password credentials using its
+ existing password validation algorithm.
+
+ Since this access token request utilizes the resource owner's
+ password, the authorization server MUST protect the endpoint against
+ brute force attacks (e.g., using rate-limitation or generating
+ alerts).
+
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ for param in ('grant_type', 'username', 'password'):
+ if not getattr(request, param):
+ raise errors.InvalidRequestError(
+ 'Request is missing %s parameter.' % param, request=request)
+
+ for param in ('grant_type', 'username', 'password', 'scope'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+ # This error should rarely (if ever) occur if requests are routed to
+ # grant type handlers based on the grant_type parameter.
+ if not request.grant_type == 'password':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ log.debug('Validating username %s.', request.username)
+ if not self.request_validator.validate_user(request.username,
+ request.password, request.client, request):
+ raise errors.InvalidGrantError(
+ 'Invalid credentials given.', request=request)
+ else:
+ if not hasattr(request.client, 'client_id'):
+ raise NotImplementedError(
+ 'Validate user must set the '
+ 'request.client.client_id attribute '
+ 'in authenticate_client.')
+ log.debug('Authorizing access to user %r.', request.user)
+
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ if request.client:
+ request.client_id = request.client_id or request.client.client_id
+ self.validate_scopes(request)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.parameters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
+
+.. _`Section 4`: http://tools.ietf.org/html/rfc6749#section-4
+"""
+from __future__ import absolute_import, unicode_literals
+
+import json
+import os
+import time
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
+from oauthlib.signals import scope_changed
+from .errors import raise_from_error, MissingTokenError, MissingTokenTypeError
+from .errors import MismatchingStateError, MissingCodeError
+from .errors import InsecureTransportError
+from .tokens import OAuth2Token
+from .utils import list_to_scope, scope_to_list, is_secure_transport
+
+
+def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
+ scope=None, state=None, **kwargs):
+ """Prepare the authorization grant request URI.
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the ``application/x-www-form-urlencoded`` format as defined by
+ [`W3C.REC-html401-19991224`_]:
+
+ :param response_type: To indicate which OAuth 2 grant/flow is required,
+ "code" and "token".
+ :param client_id: The client identifier as described in `Section 2.2`_.
+ :param redirect_uri: The client provided URI to redirect back to after
+ authorization as described in `Section 3.1.2`_.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+
+ :param state: An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent
+ back to the client. The parameter SHOULD be used for
+ preventing cross-site request forgery as described in
+ `Section 10.12`_.
+ :param kwargs: Extra arguments to embed in the grant/authorization URL.
+
+ An example of an authorization code grant authorization URL:
+
+ .. code-block:: http
+
+ GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
+ &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
+ Host: server.example.com
+
+ .. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
+ .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ params = [(('response_type', response_type)),
+ (('client_id', client_id))]
+
+ if redirect_uri:
+ params.append(('redirect_uri', redirect_uri))
+ if scope:
+ params.append(('scope', list_to_scope(scope)))
+ if state:
+ params.append(('state', state))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((unicode_type(k), kwargs[k]))
+
+ return add_params_to_uri(uri, params)
+
+
+def prepare_token_request(grant_type, body='', **kwargs):
+ """Prepare the access token request.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the ``application/x-www-form-urlencoded``
+ format in the HTTP request entity-body:
+
+ :param grant_type: To indicate grant type being used, i.e. "password",
+ "authorization_code" or "client_credentials".
+ :param body: Existing request body to embed parameters in.
+ :param code: If using authorization code grant, pass the previously
+ obtained authorization code as the ``code`` argument.
+ :param redirect_uri: If the "redirect_uri" parameter was included in the
+ authorization request as described in
+ `Section 4.1.1`_, and their values MUST be identical.
+ :param kwargs: Extra arguments to embed in the request body.
+
+ An example of an authorization code token request body:
+
+ .. code-block:: http
+
+ grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
+ &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
+
+ .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
+ """
+ params = [('grant_type', grant_type)]
+
+ if 'scope' in kwargs:
+ kwargs['scope'] = list_to_scope(kwargs['scope'])
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((unicode_type(k), kwargs[k]))
+
+ return add_params_to_qs(body, params)
+
+
+def prepare_token_revocation_request(url, token, token_type_hint="access_token",
+ callback=None, body='', **kwargs):
+ """Prepare a token revocation request.
+
+ The client constructs the request by including the following parameters
+ using the "application/x-www-form-urlencoded" format in the HTTP request
+ entity-body:
+
+ token REQUIRED. The token that the client wants to get revoked.
+
+ token_type_hint OPTIONAL. A hint about the type of the token submitted
+ for revocation. Clients MAY pass this parameter in order to help the
+ authorization server to optimize the token lookup. If the server is unable
+ to locate the token using the given hint, it MUST extend its search across
+ all of its supported token types. An authorization server MAY ignore this
+ parameter, particularly if it is able to detect the token type
+ automatically. This specification defines two such values:
+
+ * access_token: An access token as defined in [RFC6749],
+ `Section 1.4`_
+
+ * refresh_token: A refresh token as defined in [RFC6749],
+ `Section 1.5`_
+
+ Specific implementations, profiles, and extensions of this
+ specification MAY define other values for this parameter using the
+ registry defined in `Section 4.1.2`_.
+
+ .. _`Section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`Section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`Section 4.1.2`: http://tools.ietf.org/html/rfc7009#section-4.1.2
+
+ """
+ if not is_secure_transport(url):
+ raise InsecureTransportError()
+
+ params = [('token', token)]
+
+ if token_type_hint:
+ params.append(('token_type_hint', token_type_hint))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((unicode_type(k), kwargs[k]))
+
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+
+ if callback:
+ params.append(('callback', callback))
+ return add_params_to_uri(url, params), headers, body
+ else:
+ return url, headers, add_params_to_qs(body, params)
+
+
+def parse_authorization_code_response(uri, state=None):
+ """Parse authorization grant response URI into a dict.
+
+ If the resource owner grants the access request, the authorization
+ server issues an authorization code and delivers it to the client by
+ adding the following parameters to the query component of the
+ redirection URI using the ``application/x-www-form-urlencoded`` format:
+
+ **code**
+ REQUIRED. The authorization code generated by the
+ authorization server. The authorization code MUST expire
+ shortly after it is issued to mitigate the risk of leaks. A
+ maximum authorization code lifetime of 10 minutes is
+ RECOMMENDED. The client MUST NOT use the authorization code
+ more than once. If an authorization code is used more than
+ once, the authorization server MUST deny the request and SHOULD
+ revoke (when possible) all tokens previously issued based on
+ that authorization code. The authorization code is bound to
+ the client identifier and redirection URI.
+
+ **state**
+ REQUIRED if the "state" parameter was present in the client
+ authorization request. The exact value received from the
+ client.
+
+ :param uri: The full redirect URL back to the client.
+ :param state: The state parameter from the authorization request.
+
+ For example, the authorization server redirects the user-agent by
+ sending the following HTTP response:
+
+ .. code-block:: http
+
+ HTTP/1.1 302 Found
+ Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
+ &state=xyz
+
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ query = urlparse.urlparse(uri).query
+ params = dict(urlparse.parse_qsl(query))
+
+ if not 'code' in params:
+ raise MissingCodeError("Missing code parameter in response.")
+
+ if state and params.get('state', None) != state:
+ raise MismatchingStateError()
+
+ return params
+
+
+def parse_implicit_response(uri, state=None, scope=None):
+ """Parse the implicit token response URI into a dict.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ the following parameters to the fragment component of the redirection
+ URI using the ``application/x-www-form-urlencoded`` format:
+
+ **access_token**
+ REQUIRED. The access token issued by the authorization server.
+
+ **token_type**
+ REQUIRED. The type of the token issued as described in
+ Section 7.1. Value is case insensitive.
+
+ **expires_in**
+ RECOMMENDED. The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ **scope**
+ OPTIONAL, if identical to the scope requested by the client,
+ otherwise REQUIRED. The scope of the access token as described
+ by Section 3.3.
+
+ **state**
+ REQUIRED if the "state" parameter was present in the client
+ authorization request. The exact value received from the
+ client.
+
+ Similar to the authorization code response, but with a full token provided
+ in the URL fragment:
+
+ .. code-block:: http
+
+ HTTP/1.1 302 Found
+ Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
+ &state=xyz&token_type=example&expires_in=3600
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ fragment = urlparse.urlparse(uri).fragment
+ params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
+
+ if 'scope' in params:
+ params['scope'] = scope_to_list(params['scope'])
+
+ if 'expires_in' in params:
+ params['expires_at'] = time.time() + int(params['expires_in'])
+
+ if state and params.get('state', None) != state:
+ raise ValueError("Mismatching or missing state in params.")
+
+ params = OAuth2Token(params, old_scope=scope)
+ validate_token_parameters(params)
+ return params
+
+
+def parse_token_response(body, scope=None):
+ """Parse the JSON token response body into a dict.
+
+ The authorization server issues an access token and optional refresh
+ token, and constructs the response by adding the following parameters
+ to the entity body of the HTTP response with a 200 (OK) status code:
+
+ access_token
+ REQUIRED. The access token issued by the authorization server.
+ token_type
+ REQUIRED. The type of the token issued as described in
+ `Section 7.1`_. Value is case insensitive.
+ expires_in
+ RECOMMENDED. The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+ refresh_token
+ OPTIONAL. The refresh token which can be used to obtain new
+ access tokens using the same authorization grant as described
+ in `Section 6`_.
+ scope
+ OPTIONAL, if identical to the scope requested by the client,
+ otherwise REQUIRED. The scope of the access token as described
+ by `Section 3.3`_.
+
+ The parameters are included in the entity body of the HTTP response
+ using the "application/json" media type as defined by [`RFC4627`_]. The
+ parameters are serialized into a JSON structure by adding each
+ parameter at the highest structure level. Parameter names and string
+ values are included as JSON strings. Numerical values are included
+ as JSON numbers. The order of parameters does not matter and can
+ vary.
+
+ :param body: The full json encoded response body.
+ :param scope: The scope requested during authorization.
+
+ For example:
+
+ .. code-block:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Cache-Control: no-store
+ Pragma: no-cache
+
+ {
+ "access_token":"2YotnFZFEjr1zCsicMWpAA",
+ "token_type":"example",
+ "expires_in":3600,
+ "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
+ "example_parameter":"example_value"
+ }
+
+ .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
+ .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`RFC4627`: http://tools.ietf.org/html/rfc4627
+ """
+ try:
+ params = json.loads(body)
+ except ValueError:
+
+ # Fall back to URL-encoded string, to support old implementations,
+ # including (at time of writing) Facebook. See:
+ # https://github.com/idan/oauthlib/issues/267
+
+ params = dict(urlparse.parse_qsl(body))
+ for key in ('expires_in', 'expires'):
+ if key in params: # cast a couple things to int
+ params[key] = int(params[key])
+
+ if 'scope' in params:
+ params['scope'] = scope_to_list(params['scope'])
+
+ if 'expires' in params:
+ params['expires_in'] = params.pop('expires')
+
+ if 'expires_in' in params:
+ params['expires_at'] = time.time() + int(params['expires_in'])
+
+ params = OAuth2Token(params, old_scope=scope)
+ validate_token_parameters(params)
+ return params
+
+
+def validate_token_parameters(params):
+ """Ensures token precence, token type, expiration and scope in params."""
+ if 'error' in params:
+ raise_from_error(params.get('error'), params)
+
+ if not 'access_token' in params:
+ raise MissingTokenError(description="Missing access token parameter.")
+
+ if not 'token_type' in params:
+ if os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
+ raise MissingTokenTypeError()
+
+ # If the issued access token scope is different from the one requested by
+ # the client, the authorization server MUST include the "scope" response
+ # parameter to inform the client of the actual scope granted.
+ # http://tools.ietf.org/html/rfc6749#section-3.3
+ if params.scope_changed:
+ message = 'Scope has changed from "{old}" to "{new}".'.format(
+ old=params.old_scope, new=params.scope,
+ )
+ scope_changed.send(message=message, old=params.old_scopes, new=params.scopes)
+ if not os.environ.get('OAUTHLIB_RELAX_TOKEN_SCOPE', None):
+ w = Warning(message)
+ w.token = params
+ w.old_scope = params.old_scopes
+ w.new_scope = params.scopes
+ raise w
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class RequestValidator(object):
+
+ def client_authentication_required(self, request, *args, **kwargs):
+ """Determine if client authentication is required for current request.
+
+ According to the rfc6749, client authentication is required in the following cases:
+ - Resource Owner Password Credentials Grant, when Client type is Confidential or when
+ Client was issued client credentials or whenever Client provided client
+ authentication, see `Section 4.3.2`_.
+ - Authorization Code Grant, when Client type is Confidential or when Client was issued
+ client credentials or whenever Client provided client authentication,
+ see `Section 4.1.3`_.
+ - Refresh Token Grant, when Client type is Confidential or when Client was issued
+ client credentials or whenever Client provided client authentication, see
+ `Section 6`_
+
+ :param request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Resource Owner Password Credentials Grant
+ - Refresh Token Grant
+
+ .. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2
+ .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3
+ .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
+ """
+ return True
+
+ def authenticate_client(self, request, *args, **kwargs):
+ """Authenticate client through means outside the OAuth 2 spec.
+
+ Means of authentication is negotiated beforehand and may for example
+ be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization
+ header.
+
+ Headers may be accesses through request.headers and parameters found in
+ both body and query can be obtained by direct attribute access, i.e.
+ request.client_id for client_id in the URL query.
+
+ OBS! Certain grant types rely on this authentication, possibly with
+ other fallbacks, and for them to recognize this authorization please
+ set the client attribute on the request (request.client). Note that
+ preferably this client object should have a client_id attribute of
+ unicode type (request.client.client_id).
+
+ :param request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Resource Owner Password Credentials Grant (may be disabled)
+ - Client Credentials Grant
+ - Refresh Token Grant
+
+ .. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def authenticate_client_id(self, client_id, request, *args, **kwargs):
+ """Ensure client_id belong to a non-confidential client.
+
+ A non-confidential client is one that is not required to authenticate
+ through other means, such as using HTTP Basic.
+
+ Note, while not strictly necessary it can often be very convenient
+ to set request.client to the client object associated with the
+ given client_id.
+
+ :param request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
+ *args, **kwargs):
+ """Ensure client is authorized to redirect to the redirect_uri requested.
+
+ If the client specifies a redirect_uri when obtaining code then
+ that redirect URI must be bound to the code and verified equal
+ in this method.
+
+ All clients should register the absolute URIs of all URIs they intend
+ to redirect to. The registration is outside of the scope of oauthlib.
+
+ :param client_id: Unicode client identifier
+ :param code: Unicode authorization_code.
+ :param redirect_uri: Unicode absolute URI
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant (during token request)
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
+ """Get the default redirect URI for the client.
+
+ :param client_id: Unicode client identifier
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: The default redirect URI for the client
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_default_scopes(self, client_id, request, *args, **kwargs):
+ """Get the default scopes for the client.
+
+ :param client_id: Unicode client identifier
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: List of default scopes
+
+ Method is used by all core grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_original_scopes(self, refresh_token, request, *args, **kwargs):
+ """Get the list of scopes associated with the refresh token.
+
+ :param refresh_token: Unicode refresh token
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: List of scopes.
+
+ Method is used by:
+ - Refresh token grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def is_within_original_scope(self, request_scopes, refresh_token, request, *args, **kwargs):
+ """Check if requested scopes are within a scope of the refresh token.
+
+ When access tokens are refreshed the scope of the new token
+ needs to be within the scope of the original token. This is
+ ensured by checking that all requested scopes strings are on
+ the list returned by the get_original_scopes. If this check
+ fails, is_within_original_scope is called. The method can be
+ used in situations where returning all valid scopes from the
+ get_original_scopes is not practical.
+
+ :param request_scopes: A list of scopes that were requested by client
+ :param refresh_token: Unicode refresh_token
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Refresh token grant
+ """
+ return False
+
+ def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
+ """Invalidate an authorization code after use.
+
+ :param client_id: Unicode client identifier
+ :param code: The authorization code grant (request.code).
+ :param request: The HTTP Request (oauthlib.common.Request)
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
+ """Revoke an access or refresh token.
+
+ :param token: The token string.
+ :param token_type_hint: access_token or refresh_token.
+ :param request: The HTTP Request (oauthlib.common.Request)
+
+ Method is used by:
+ - Revocation Endpoint
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def rotate_refresh_token(self, request):
+ """Determine whether to rotate the refresh token. Default, yes.
+
+ When access tokens are refreshed the old refresh token can be kept
+ or replaced with a new one (rotated). Return True to rotate and
+ and False for keeping original.
+
+ :param request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Refresh Token Grant
+ """
+ return True
+
+ def save_authorization_code(self, client_id, code, request, *args, **kwargs):
+ """Persist the authorization_code.
+
+ The code should at minimum be associated with:
+ - a client and it's client_id
+ - the redirect URI used (request.redirect_uri)
+ - whether the redirect URI used is the client default or not
+ - a resource owner / user (request.user)
+ - authorized scopes (request.scopes)
+
+ The authorization code grant dict (code) holds at least the key 'code'::
+
+ {'code': 'sdf345jsdf0934f'}
+
+ :param client_id: Unicode client identifier
+ :param code: A dict of the authorization code grant.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: The default redirect URI for the client
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def save_bearer_token(self, token, request, *args, **kwargs):
+ """Persist the Bearer token.
+
+ The Bearer token should at minimum be associated with:
+ - a client and it's client_id, if available
+ - a resource owner / user (request.user)
+ - authorized scopes (request.scopes)
+ - an expiration time
+ - a refresh token, if issued
+
+ The Bearer token dict may hold a number of items::
+
+ {
+ 'token_type': 'Bearer',
+ 'access_token': 'askfjh234as9sd8',
+ 'expires_in': 3600,
+ 'scope': 'string of space separated authorized scopes',
+ 'refresh_token': '23sdf876234', # if issued
+ 'state': 'given_by_client', # if supplied by client
+ }
+
+ Note that while "scope" is a string-separated list of authorized scopes,
+ the original list is still available in request.scopes
+
+ :param client_id: Unicode client identifier
+ :param token: A Bearer token dict
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: The default redirect URI for the client
+
+ Method is used by all core grant types issuing Bearer tokens:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant (might not associate a client)
+ - Client Credentials grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_bearer_token(self, token, scopes, request):
+ """Ensure the Bearer token is valid and authorized access to scopes.
+
+ :param token: A string of random characters.
+ :param scopes: A list of scopes associated with the protected resource.
+ :param request: The HTTP Request (oauthlib.common.Request)
+
+ A key to OAuth 2 security and restricting impact of leaked tokens is
+ the short expiration time of tokens, *always ensure the token has not
+ expired!*.
+
+ Two different approaches to scope validation:
+
+ 1) all(scopes). The token must be authorized access to all scopes
+ associated with the resource. For example, the
+ token has access to ``read-only`` and ``images``,
+ thus the client can view images but not upload new.
+ Allows for fine grained access control through
+ combining various scopes.
+
+ 2) any(scopes). The token must be authorized access to one of the
+ scopes associated with the resource. For example,
+ token has access to ``read-only-images``.
+ Allows for fine grained, although arguably less
+ convenient, access control.
+
+ A powerful way to use scopes would mimic UNIX ACLs and see a scope
+ as a group with certain privileges. For a restful API these might
+ map to HTTP verbs instead of read, write and execute.
+
+ Note, the request.user attribute can be set to the resource owner
+ associated with this token. Similarly the request.client and
+ request.scopes attribute can be set to associated client object
+ and authorized scopes. If you then use a decorator such as the
+ one provided for django these attributes will be made available
+ in all protected views as keyword arguments.
+
+ :param token: Unicode Bearer token
+ :param scopes: List of scopes (defined by you)
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is indirectly used by all core Bearer token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_client_id(self, client_id, request, *args, **kwargs):
+ """Ensure client_id belong to a valid and active client.
+
+ Note, while not strictly necessary it can often be very convenient
+ to set request.client to the client object associated with the
+ given client_id.
+
+ :param request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_code(self, client_id, code, client, request, *args, **kwargs):
+ """Ensure the authorization_code is valid and assigned to client.
+
+ OBS! The request.user attribute should be set to the resource owner
+ associated with this authorization code. Similarly request.scopes and
+ request.state must also be set.
+
+ :param client_id: Unicode client identifier
+ :param code: Unicode authorization code
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
+ """Ensure client is authorized to use the grant_type requested.
+
+ :param client_id: Unicode client identifier
+ :param grant_type: Unicode grant type, i.e. authorization_code, password.
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials Grant
+ - Refresh Token Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
+ """Ensure client is authorized to redirect to the redirect_uri requested.
+
+ All clients should register the absolute URIs of all URIs they intend
+ to redirect to. The registration is outside of the scope of oauthlib.
+
+ :param client_id: Unicode client identifier
+ :param redirect_uri: Unicode absolute URI
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
+ """Ensure the Bearer token is valid and authorized access to scopes.
+
+ OBS! The request.user attribute should be set to the resource owner
+ associated with this refresh token.
+
+ :param refresh_token: Unicode refresh token
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant (indirectly by issuing refresh tokens)
+ - Resource Owner Password Credentials Grant (also indirectly)
+ - Refresh Token Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
+ """Ensure client is authorized to use the response_type requested.
+
+ :param client_id: Unicode client identifier
+ :param response_type: Unicode response type, i.e. code, token.
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
+ """Ensure the client is authorized access to requested scopes.
+
+ :param client_id: Unicode client identifier
+ :param scopes: List of scopes (defined by you)
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by all core grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_user(self, username, password, client, request, *args, **kwargs):
+ """Ensure the username and password is valid.
+
+ OBS! The validation should also set the user attribute of the request
+ to a valid resource owner, i.e. request.user = username or similar. If
+ not set you will be unable to associate a token with a user in the
+ persistance method used (commonly, save_bearer_token).
+
+ :param username: Unicode username
+ :param password: Unicode password
+ :param client: Client object set by you, see authenticate_client.
+ :param request: The HTTP Request (oauthlib.common.Request)
+ :rtype: True or False
+
+ Method is used by:
+ - Resource Owner Password Credentials Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
--- /dev/null
+"""
+oauthlib.oauth2.rfc6749.tokens
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods for adding two types of access tokens to requests.
+
+- Bearer http://tools.ietf.org/html/rfc6750
+- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+"""
+from __future__ import absolute_import, unicode_literals
+
+from binascii import b2a_base64
+import hashlib
+import hmac
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
+from oauthlib import common
+
+from . import utils
+
+
+class OAuth2Token(dict):
+
+ def __init__(self, params, old_scope=None):
+ super(OAuth2Token, self).__init__(params)
+ self._new_scope = None
+ if 'scope' in params:
+ self._new_scope = set(utils.scope_to_list(params['scope']))
+ if old_scope is not None:
+ self._old_scope = set(utils.scope_to_list(old_scope))
+ if self._new_scope is None:
+ # the rfc says that if the scope hasn't changed, it's optional
+ # in params so set the new scope to the old scope
+ self._new_scope = self._old_scope
+ else:
+ self._old_scope = self._new_scope
+
+ @property
+ def scope_changed(self):
+ return self._new_scope != self._old_scope
+
+ @property
+ def old_scope(self):
+ return utils.list_to_scope(self._old_scope)
+
+ @property
+ def old_scopes(self):
+ return list(self._old_scope)
+
+ @property
+ def scope(self):
+ return utils.list_to_scope(self._new_scope)
+
+ @property
+ def scopes(self):
+ return list(self._new_scope)
+
+ @property
+ def missing_scopes(self):
+ return list(self._old_scope - self._new_scope)
+
+ @property
+ def additional_scopes(self):
+ return list(self._new_scope - self._old_scope)
+
+
+def prepare_mac_header(token, uri, key, http_method,
+ nonce=None,
+ headers=None,
+ body=None,
+ ext='',
+ hash_algorithm='hmac-sha-1',
+ issue_time=None,
+ draft=0):
+ """Add an `MAC Access Authentication`_ signature to headers.
+
+ Unlike OAuth 1, this HMAC signature does not require inclusion of the
+ request payload/body, neither does it use a combination of client_secret
+ and token_secret but rather a mac_key provided together with the access
+ token.
+
+ Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
+ `extension algorithms`_ are not supported.
+
+ Example MAC Authorization header, linebreaks added for clarity
+
+ Authorization: MAC id="h480djs93hd8",
+ nonce="1336363200:dj83hs9s",
+ mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
+
+ .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+ .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
+
+ :param uri: Request URI.
+ :param headers: Request headers as a dictionary.
+ :param http_method: HTTP Request method.
+ :param key: MAC given provided by token endpoint.
+ :param hash_algorithm: HMAC algorithm provided by token endpoint.
+ :param issue_time: Time when the MAC credentials were issued (datetime).
+ :param draft: MAC authentication specification version.
+ :return: headers dictionary with the authorization field added.
+ """
+ http_method = http_method.upper()
+ host, port = utils.host_from_uri(uri)
+
+ if hash_algorithm.lower() == 'hmac-sha-1':
+ h = hashlib.sha1
+ elif hash_algorithm.lower() == 'hmac-sha-256':
+ h = hashlib.sha256
+ else:
+ raise ValueError('unknown hash algorithm')
+
+ if draft == 0:
+ nonce = nonce or '{0}:{1}'.format(utils.generate_age(issue_time),
+ common.generate_nonce())
+ else:
+ ts = common.generate_timestamp()
+ nonce = common.generate_nonce()
+
+ sch, net, path, par, query, fra = urlparse(uri)
+
+ if query:
+ request_uri = path + '?' + query
+ else:
+ request_uri = path
+
+ # Hash the body/payload
+ if body is not None and draft == 0:
+ body = body.encode('utf-8')
+ bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
+ else:
+ bodyhash = ''
+
+ # Create the normalized base string
+ base = []
+ if draft == 0:
+ base.append(nonce)
+ else:
+ base.append(ts)
+ base.append(nonce)
+ base.append(http_method.upper())
+ base.append(request_uri)
+ base.append(host)
+ base.append(port)
+ if draft == 0:
+ base.append(bodyhash)
+ base.append(ext or '')
+ base_string = '\n'.join(base) + '\n'
+
+ # hmac struggles with unicode strings - http://bugs.python.org/issue5285
+ if isinstance(key, unicode_type):
+ key = key.encode('utf-8')
+ sign = hmac.new(key, base_string.encode('utf-8'), h)
+ sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
+
+ header = []
+ header.append('MAC id="%s"' % token)
+ if draft != 0:
+ header.append('ts="%s"' % ts)
+ header.append('nonce="%s"' % nonce)
+ if bodyhash:
+ header.append('bodyhash="%s"' % bodyhash)
+ if ext:
+ header.append('ext="%s"' % ext)
+ header.append('mac="%s"' % sign)
+
+ headers = headers or {}
+ headers['Authorization'] = ', '.join(header)
+ return headers
+
+
+def prepare_bearer_uri(token, uri):
+ """Add a `Bearer Token`_ to the request URI.
+ Not recommended, use only if client can't use authorization header or body.
+
+ http://www.example.com/path?access_token=h480djs93hd8
+
+ .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+ """
+ return add_params_to_uri(uri, [(('access_token', token))])
+
+
+def prepare_bearer_headers(token, headers=None):
+ """Add a `Bearer Token`_ to the request URI.
+ Recommended method of passing bearer tokens.
+
+ Authorization: Bearer h480djs93hd8
+
+ .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+ """
+ headers = headers or {}
+ headers['Authorization'] = 'Bearer %s' % token
+ return headers
+
+
+def prepare_bearer_body(token, body=''):
+ """Add a `Bearer Token`_ to the request body.
+
+ access_token=h480djs93hd8
+
+ .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+ """
+ return add_params_to_qs(body, [(('access_token', token))])
+
+
+def random_token_generator(request, refresh_token=False):
+ return common.generate_token()
+
+
+def signed_token_generator(private_pem, **kwargs):
+ def signed_token_generator(request):
+ request.claims = kwargs
+ return common.generate_signed_token(private_pem, request)
+
+ return signed_token_generator
+
+
+class TokenBase(object):
+
+ def __call__(self, request, refresh_token=False):
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_request(self, request):
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def estimate_type(self, request):
+ raise NotImplementedError('Subclasses must implement this method.')
+
+
+class BearerToken(TokenBase):
+
+ def __init__(self, request_validator=None, token_generator=None,
+ expires_in=None, refresh_token_generator=None):
+ self.request_validator = request_validator
+ self.token_generator = token_generator or random_token_generator
+ self.refresh_token_generator = (
+ refresh_token_generator or self.token_generator
+ )
+ self.expires_in = expires_in or 3600
+
+ def create_token(self, request, refresh_token=False):
+ """Create a BearerToken, by default without refresh token."""
+
+ if callable(self.expires_in):
+ expires_in = self.expires_in(request)
+ else:
+ expires_in = self.expires_in
+
+ request.expires_in = expires_in
+
+ token = {
+ 'access_token': self.token_generator(request),
+ 'expires_in': expires_in,
+ 'token_type': 'Bearer',
+ }
+
+ if request.scopes is not None:
+ token['scope'] = ' '.join(request.scopes)
+
+ if request.state is not None:
+ token['state'] = request.state
+
+ if refresh_token:
+ if (request.refresh_token and
+ not self.request_validator.rotate_refresh_token(request)):
+ token['refresh_token'] = request.refresh_token
+ else:
+ token['refresh_token'] = self.refresh_token_generator(request)
+
+ token.update(request.extra_credentials or {})
+ token = OAuth2Token(token)
+ self.request_validator.save_bearer_token(token, request)
+ return token
+
+ def validate_request(self, request):
+ token = None
+ if 'Authorization' in request.headers:
+ token = request.headers.get('Authorization')[7:]
+ else:
+ token = request.access_token
+ return self.request_validator.validate_bearer_token(
+ token, request.scopes, request)
+
+ def estimate_type(self, request):
+ if request.headers.get('Authorization', '').startswith('Bearer'):
+ return 9
+ elif request.access_token is not None:
+ return 5
+ else:
+ return 0
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth 2 spec.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import os
+import datetime
+try:
+ from urllib import quote
+except ImportError:
+ from urllib.parse import quote
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+from oauthlib.common import unicode_type, urldecode
+
+
+def list_to_scope(scope):
+ """Convert a list of scopes to a space separated string."""
+ if isinstance(scope, unicode_type) or scope is None:
+ return scope
+ elif isinstance(scope, (tuple, list)):
+ return " ".join([unicode_type(s) for s in scope])
+ elif isinstance(scope, set):
+ return list_to_scope(list(scope))
+ else:
+ raise ValueError("Invalid scope, must be string or list.")
+
+
+def scope_to_list(scope):
+ """Convert a space separated string to a list of scopes."""
+ if isinstance(scope, list):
+ return [unicode_type(s) for s in scope]
+ if isinstance(scope, set):
+ scope_to_list(list(scope))
+ elif scope is None:
+ return None
+ else:
+ return scope.strip().split(" ")
+
+
+def params_from_uri(uri):
+ params = dict(urldecode(urlparse(uri).query))
+ if 'scope' in params:
+ params['scope'] = scope_to_list(params['scope'])
+ return params
+
+
+def host_from_uri(uri):
+ """Extract hostname and port from URI.
+
+ Will use default port for HTTP and HTTPS if none is present in the URI.
+ """
+ default_ports = {
+ 'HTTP': '80',
+ 'HTTPS': '443',
+ }
+
+ sch, netloc, path, par, query, fra = urlparse(uri)
+ if ':' in netloc:
+ netloc, port = netloc.split(':', 1)
+ else:
+ port = default_ports.get(sch.upper())
+
+ return netloc, port
+
+
+def escape(u):
+ """Escape a string in an OAuth-compatible fashion.
+
+ TODO: verify whether this can in fact be used for OAuth 2
+
+ """
+ if not isinstance(u, unicode_type):
+ raise ValueError('Only unicode objects are escapable.')
+ return quote(u.encode('utf-8'), safe=b'~')
+
+
+def generate_age(issue_time):
+ """Generate a age parameter for MAC authentication draft 00."""
+ td = datetime.datetime.now() - issue_time
+ age = (td.microseconds + (td.seconds + td.days * 24 * 3600)
+ * 10 ** 6) / 10 ** 6
+ return unicode_type(age)
+
+
+def is_secure_transport(uri):
+ """Check if the uri is over ssl."""
+ if os.environ.get('OAUTHLIB_INSECURE_TRANSPORT'):
+ return True
+ return uri.lower().startswith('https://')
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+ Implements signals based on blinker if available, otherwise
+ falls silently back to a noop. Shamelessly stolen from flask.signals:
+ https://github.com/mitsuhiko/flask/blob/master/flask/signals.py
+"""
+signals_available = False
+try:
+ from blinker import Namespace
+ signals_available = True
+except ImportError:
+ class Namespace(object):
+ def signal(self, name, doc=None):
+ return _FakeSignal(name, doc)
+
+ class _FakeSignal(object):
+ """If blinker is unavailable, create a fake class with the same
+ interface that allows sending of signals but will fail with an
+ error on anything else. Instead of doing anything on send, it
+ will just ignore the arguments and do nothing instead.
+ """
+
+ def __init__(self, name, doc=None):
+ self.name = name
+ self.__doc__ = doc
+ def _fail(self, *args, **kwargs):
+ raise RuntimeError('signalling support is unavailable '
+ 'because the blinker library is '
+ 'not installed.')
+ send = lambda *a, **kw: None
+ connect = disconnect = has_receivers_for = receivers_for = \
+ temporarily_connected_to = connected_to = _fail
+ del _fail
+
+# The namespace for code signals. If you are not oauthlib code, do
+# not put signals in here. Create your own namespace instead.
+_signals = Namespace()
+
+
+# Core signals.
+scope_changed = _signals.signal('scope-changed')
--- /dev/null
+"""
+Regex for URIs
+
+These regex are directly derived from the collected ABNF in RFC3986
+(except for DIGIT, ALPHA and HEXDIG, defined by RFC2234).
+
+They should be processed with re.VERBOSE.
+
+Thanks Mark Nottingham for this code - https://gist.github.com/138549
+"""
+from __future__ import unicode_literals
+import re
+
+# basics
+
+DIGIT = r"[\x30-\x39]"
+
+ALPHA = r"[\x41-\x5A\x61-\x7A]"
+
+HEXDIG = r"[\x30-\x39A-Fa-f]"
+
+# pct-encoded = "%" HEXDIG HEXDIG
+pct_encoded = r" %% %(HEXDIG)s %(HEXDIG)s" % locals()
+
+# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+unreserved = r"(?: %(ALPHA)s | %(DIGIT)s | \- | \. | _ | ~ )" % locals()
+
+# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+gen_delims = r"(?: : | / | \? | \# | \[ | \] | @ )"
+
+# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+# / "*" / "+" / "," / ";" / "="
+sub_delims = r"""(?: ! | \$ | & | ' | \( | \) |
+ \* | \+ | , | ; | = )"""
+
+# pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+pchar = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : | @ )" % locals(
+)
+
+# reserved = gen-delims / sub-delims
+reserved = r"(?: %(gen_delims)s | %(sub_delims)s )" % locals()
+
+
+# scheme
+
+# scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+scheme = r"%(ALPHA)s (?: %(ALPHA)s | %(DIGIT)s | \+ | \- | \. )*" % locals()
+
+
+# authority
+
+# dec-octet = DIGIT ; 0-9
+# / %x31-39 DIGIT ; 10-99
+# / "1" 2DIGIT ; 100-199
+# / "2" %x30-34 DIGIT ; 200-249
+# / "25" %x30-35 ; 250-255
+dec_octet = r"""(?: %(DIGIT)s |
+ [\x31-\x39] %(DIGIT)s |
+ 1 %(DIGIT)s{2} |
+ 2 [\x30-\x34] %(DIGIT)s |
+ 25 [\x30-\x35]
+ )
+""" % locals()
+
+# IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
+IPv4address = r"%(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s" % locals(
+)
+
+# h16 = 1*4HEXDIG
+h16 = r"(?: %(HEXDIG)s ){1,4}" % locals()
+
+# ls32 = ( h16 ":" h16 ) / IPv4address
+ls32 = r"(?: (?: %(h16)s : %(h16)s ) | %(IPv4address)s )" % locals()
+
+# IPv6address = 6( h16 ":" ) ls32
+# / "::" 5( h16 ":" ) ls32
+# / [ h16 ] "::" 4( h16 ":" ) ls32
+# / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
+# / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
+# / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
+# / [ *4( h16 ":" ) h16 ] "::" ls32
+# / [ *5( h16 ":" ) h16 ] "::" h16
+# / [ *6( h16 ":" ) h16 ] "::"
+IPv6address = r"""(?: (?: %(h16)s : ){6} %(ls32)s |
+ :: (?: %(h16)s : ){5} %(ls32)s |
+ %(h16)s :: (?: %(h16)s : ){4} %(ls32)s |
+ (?: %(h16)s : ) %(h16)s :: (?: %(h16)s : ){3} %(ls32)s |
+ (?: %(h16)s : ){2} %(h16)s :: (?: %(h16)s : ){2} %(ls32)s |
+ (?: %(h16)s : ){3} %(h16)s :: %(h16)s : %(ls32)s |
+ (?: %(h16)s : ){4} %(h16)s :: %(ls32)s |
+ (?: %(h16)s : ){5} %(h16)s :: %(h16)s |
+ (?: %(h16)s : ){6} %(h16)s ::
+ )
+""" % locals()
+
+# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals()
+
+# IP-literal = "[" ( IPv6address / IPvFuture ) "]"
+IP_literal = r"\[ (?: %(IPv6address)s | %(IPvFuture)s ) \]" % locals()
+
+# reg-name = *( unreserved / pct-encoded / sub-delims )
+reg_name = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s )*" % locals()
+
+# userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+userinfo = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : )" % locals(
+)
+
+# host = IP-literal / IPv4address / reg-name
+host = r"(?: %(IP_literal)s | %(IPv4address)s | %(reg_name)s )" % locals()
+
+# port = *DIGIT
+port = r"(?: %(DIGIT)s )*" % locals()
+
+# authority = [ userinfo "@" ] host [ ":" port ]
+authority = r"(?: %(userinfo)s @)? %(host)s (?: : %(port)s)?" % locals()
+
+# Path
+
+# segment = *pchar
+segment = r"%(pchar)s*" % locals()
+
+# segment-nz = 1*pchar
+segment_nz = r"%(pchar)s+" % locals()
+
+# segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+# ; non-zero-length segment without any colon ":"
+segment_nz_nc = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | @ )+" % locals()
+
+# path-abempty = *( "/" segment )
+path_abempty = r"(?: / %(segment)s )*" % locals()
+
+# path-absolute = "/" [ segment-nz *( "/" segment ) ]
+path_absolute = r"/ (?: %(segment_nz)s (?: / %(segment)s )* )?" % locals()
+
+# path-noscheme = segment-nz-nc *( "/" segment )
+path_noscheme = r"%(segment_nz_nc)s (?: / %(segment)s )*" % locals()
+
+# path-rootless = segment-nz *( "/" segment )
+path_rootless = r"%(segment_nz)s (?: / %(segment)s )*" % locals()
+
+# path-empty = 0<pchar>
+path_empty = r"" # FIXME
+
+# path = path-abempty ; begins with "/" or is empty
+# / path-absolute ; begins with "/" but not "//"
+# / path-noscheme ; begins with a non-colon segment
+# / path-rootless ; begins with a segment
+# / path-empty ; zero characters
+path = r"""(?: %(path_abempty)s |
+ %(path_absolute)s |
+ %(path_noscheme)s |
+ %(path_rootless)s |
+ %(path_empty)s
+ )
+""" % locals()
+
+### Query and Fragment
+
+# query = *( pchar / "/" / "?" )
+query = r"(?: %(pchar)s | / | \? )*" % locals()
+
+# fragment = *( pchar / "/" / "?" )
+fragment = r"(?: %(pchar)s | / | \? )*" % locals()
+
+# URIs
+
+# hier-part = "//" authority path-abempty
+# / path-absolute
+# / path-rootless
+# / path-empty
+hier_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
+ %(path_absolute)s |
+ %(path_rootless)s |
+ %(path_empty)s
+ )
+""" % locals()
+
+# relative-part = "//" authority path-abempty
+# / path-absolute
+# / path-noscheme
+# / path-empty
+relative_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
+ %(path_absolute)s |
+ %(path_noscheme)s |
+ %(path_empty)s
+ )
+""" % locals()
+
+# relative-ref = relative-part [ "?" query ] [ "#" fragment ]
+relative_ref = r"%(relative_part)s (?: \? %(query)s)? (?: \# %(fragment)s)?" % locals(
+)
+
+# URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s )? )$" % locals(
+)
+
+# URI-reference = URI / relative-ref
+URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
+
+# absolute-URI = scheme ":" hier-part [ "?" query ]
+absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals(
+)
+
+
+def is_uri(uri):
+ return re.match(URI, uri, re.VERBOSE)
+
+
+def is_uri_reference(uri):
+ return re.match(URI_reference, uri, re.VERBOSE)
+
+
+def is_absolute_uri(uri):
+ return re.match(absolute_URI, uri, re.VERBOSE)
--- /dev/null
+from .oauth1_auth import OAuth1
+from .oauth1_session import OAuth1Session
+from .oauth2_auth import OAuth2
+from .oauth2_session import OAuth2Session, TokenUpdated
+
+__version__ = '0.4.2'
+
+import requests
+if requests.__version__ < '2.0.0':
+ msg = ('You are using requests version %s, which is older than '
+ 'requests-oauthlib expects, please upgrade to 2.0.0 or later.')
+ raise Warning(msg % requests.__version__)
+
+import logging
+try: # Python 2.7+
+ from logging import NullHandler
+except ImportError:
+ class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+logging.getLogger('requests_oauthlib').addHandler(NullHandler())
--- /dev/null
+from __future__ import absolute_import
+
+from .facebook import facebook_compliance_fix
+from .linkedin import linkedin_compliance_fix
+from .weibo import weibo_compliance_fix
--- /dev/null
+import json
+
+from oauthlib.common import to_unicode
+
+
+def douban_compliance_fix(session):
+
+ def fix_token_type(r):
+ token = json.loads(r.text)
+ token.setdefault('token_type', 'Bearer')
+ fixed_token = json.dumps(token)
+ r._content = to_unicode(fixed_token).encode('utf-8')
+ return r
+
+ session._client_default_token_placement = 'query'
+ session.register_compliance_hook('access_token_response', fix_token_type)
+
+ return session
--- /dev/null
+from json import dumps
+try:
+ from urlparse import parse_qsl
+except ImportError:
+ from urllib.parse import parse_qsl
+
+from oauthlib.common import to_unicode
+
+
+def facebook_compliance_fix(session):
+
+ def _compliance_fix(r):
+ # if Facebook claims to be sending us json, let's trust them.
+ if 'application/json' in r.headers.get('content-type', {}):
+ return r
+
+ # Facebook returns a content-type of text/plain when sending their
+ # x-www-form-urlencoded responses, along with a 200. If not, let's
+ # assume we're getting JSON and bail on the fix.
+ if 'text/plain' in r.headers.get('content-type', {}) and r.status_code == 200:
+ token = dict(parse_qsl(r.text, keep_blank_values=True))
+ else:
+ return r
+
+ expires = token.get('expires')
+ if expires is not None:
+ token['expires_in'] = expires
+ token['token_type'] = 'Bearer'
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ session.register_compliance_hook('access_token_response', _compliance_fix)
+ return session
--- /dev/null
+from json import loads, dumps
+
+from oauthlib.common import add_params_to_uri, to_unicode
+
+
+def linkedin_compliance_fix(session):
+
+ def _missing_token_type(r):
+ token = loads(r.text)
+ token['token_type'] = 'Bearer'
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ def _non_compliant_param_name(url, headers, data):
+ token = [('oauth2_access_token', session._client.access_token)]
+ url = add_params_to_uri(url, token)
+ return url, headers, data
+
+ session._client.default_token_placement = 'query'
+ session.register_compliance_hook('access_token_response',
+ _missing_token_type)
+ session.register_compliance_hook('protected_request',
+ _non_compliant_param_name)
+ return session
--- /dev/null
+from json import loads, dumps
+
+from oauthlib.common import to_unicode
+
+
+def weibo_compliance_fix(session):
+
+ def _missing_token_type(r):
+ token = loads(r.text)
+ token['token_type'] = 'Bearer'
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ session._client.default_token_placement = 'query'
+ session.register_compliance_hook('access_token_response',
+ _missing_token_type)
+ return session
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import logging
+
+from oauthlib.common import extract_params
+from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER
+from oauthlib.oauth1 import SIGNATURE_TYPE_BODY
+from requests.compat import is_py3
+from requests.utils import to_native_string
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+
+if is_py3:
+ unicode = str
+
+log = logging.getLogger(__name__)
+
+# OBS!: Correct signing of requests are conditional on invoking OAuth1
+# as the last step of preparing a request, or at least having the
+# content-type set properly.
+class OAuth1(object):
+ """Signs the request using OAuth 1 (RFC5849)"""
+
+ client_class = Client
+
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None, verifier=None,
+ decoding='utf-8',
+ client_class=None,
+ force_include_body=False,
+ **kwargs):
+
+ try:
+ signature_type = signature_type.upper()
+ except AttributeError:
+ pass
+
+ client_class = client_class or self.client_class
+
+ self.force_include_body = force_include_body
+
+ self.client = client_class(client_key, client_secret, resource_owner_key,
+ resource_owner_secret, callback_uri, signature_method,
+ signature_type, rsa_key, verifier, decoding=decoding, **kwargs)
+
+ def __call__(self, r):
+ """Add OAuth parameters to the request.
+
+ Parameters may be included from the body if the content-type is
+ urlencoded, if no content type is set a guess is made.
+ """
+ # Overwriting url is safe here as request will not modify it past
+ # this point.
+ log.debug('Signing request %s using client %s', r, self.client)
+
+ content_type = r.headers.get('Content-Type', '')
+ if (not content_type and extract_params(r.body)
+ or self.client.signature_type == SIGNATURE_TYPE_BODY):
+ content_type = CONTENT_TYPE_FORM_URLENCODED
+ if not isinstance(content_type, unicode):
+ content_type = content_type.decode('utf-8')
+
+ is_form_encoded = (CONTENT_TYPE_FORM_URLENCODED in content_type)
+
+ log.debug('Including body in call to sign: %s',
+ is_form_encoded or self.force_include_body)
+
+ if is_form_encoded:
+ r.headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED
+ r.url, headers, r.body = self.client.sign(
+ unicode(r.url), unicode(r.method), r.body or '', r.headers)
+ elif self.force_include_body:
+ # To allow custom clients to work on non form encoded bodies.
+ r.url, headers, r.body = self.client.sign(
+ unicode(r.url), unicode(r.method), r.body or '', r.headers)
+ else:
+ # Omit body data in the signing of non form-encoded requests
+ r.url, headers, _ = self.client.sign(
+ unicode(r.url), unicode(r.method), None, r.headers)
+
+ r.prepare_headers(headers)
+ r.url = to_native_string(r.url)
+ log.debug('Updated url: %s', r.url)
+ log.debug('Updated headers: %s', headers)
+ log.debug('Updated body: %r', r.body)
+ return r
--- /dev/null
+from __future__ import unicode_literals
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+import logging
+
+from oauthlib.common import add_params_to_uri
+from oauthlib.common import urldecode as _urldecode
+from oauthlib.oauth1 import (
+ SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER
+)
+import requests
+
+from . import OAuth1
+
+import sys
+if sys.version > "3":
+ unicode = str
+
+
+log = logging.getLogger(__name__)
+
+
+def urldecode(body):
+ """Parse query or json to python dictionary"""
+ try:
+ return _urldecode(body)
+ except:
+ import json
+ return json.loads(body)
+
+
+class TokenRequestDenied(ValueError):
+
+ def __init__(self, message, status_code):
+ super(TokenRequestDenied, self).__init__(message)
+ self.status_code = status_code
+
+
+class TokenMissing(ValueError):
+ def __init__(self, message, response):
+ super(TokenMissing, self).__init__(message)
+ self.response = response
+
+
+class VerifierMissing(ValueError):
+ pass
+
+
+class OAuth1Session(requests.Session):
+ """Request signing and convenience methods for the oauth dance.
+
+ What is the difference between OAuth1Session and OAuth1?
+
+ OAuth1Session actually uses OAuth1 internally and its purpose is to assist
+ in the OAuth workflow through convenience methods to prepare authorization
+ URLs and parse the various token and redirection responses. It also provide
+ rudimentary validation of responses.
+
+ An example of the OAuth workflow using a basic CLI app and Twitter.
+
+ >>> # Credentials obtained during the registration.
+ >>> client_key = 'client key'
+ >>> client_secret = 'secret'
+ >>> callback_uri = 'https://127.0.0.1/callback'
+ >>>
+ >>> # Endpoints found in the OAuth provider API documentation
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+ >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
+ >>>
+ >>> oauth_session = OAuth1Session(client_key,client_secret=client_secret, callback_uri=callback_uri)
+ >>>
+ >>> # First step, fetch the request token.
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'kjerht2309u',
+ 'oauth_token_secret': 'lsdajfh923874',
+ }
+ >>>
+ >>> # Second step. Follow this link and authorize
+ >>> oauth_session.authorization_url(authorization_url)
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
+ >>>
+ >>> # Third step. Fetch the access token
+ >>> redirect_response = raw_input('Paste the full redirect URL here.')
+ >>> oauth_session.parse_authorization_response(redirect_response)
+ {
+ 'oauth_token: 'kjerht2309u',
+ 'oauth_token_secret: 'lsdajfh923874',
+ 'oauth_verifier: 'w34o8967345',
+ }
+ >>> oauth_session.fetch_access_token(access_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ >>> # Done. You can now make OAuth requests.
+ >>> status_url = 'http://api.twitter.com/1/statuses/update.json'
+ >>> new_status = {'status': 'hello world!'}
+ >>> oauth_session.post(status_url, data=new_status)
+ <Response [200]>
+ """
+
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None,
+ verifier=None,
+ client_class=None,
+ force_include_body=False,
+ **kwargs):
+ """Construct the OAuth 1 session.
+
+ :param client_key: A client specific identifier.
+ :param client_secret: A client specific secret used to create HMAC and
+ plaintext signatures.
+ :param resource_owner_key: A resource owner key, also referred to as
+ request token or access token depending on
+ when in the workflow it is used.
+ :param resource_owner_secret: A resource owner secret obtained with
+ either a request or access token. Often
+ referred to as token secret.
+ :param callback_uri: The URL the user is redirect back to after
+ authorization.
+ :param signature_method: Signature methods determine how the OAuth
+ signature is created. The three options are
+ oauthlib.oauth1.SIGNATURE_HMAC (default),
+ oauthlib.oauth1.SIGNATURE_RSA and
+ oauthlib.oauth1.SIGNATURE_PLAIN.
+ :param signature_type: Signature type decides where the OAuth
+ parameters are added. Either in the
+ Authorization header (default) or to the URL
+ query parameters or the request body. Defined as
+ oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER,
+ oauthlib.oauth1.SIGNATURE_TYPE_QUERY and
+ oauthlib.oauth1.SIGNATURE_TYPE_BODY
+ respectively.
+ :param rsa_key: The private RSA key as a string. Can only be used with
+ signature_method=oauthlib.oauth1.SIGNATURE_RSA.
+ :param verifier: A verifier string to prove authorization was granted.
+ :param client_class: A subclass of `oauthlib.oauth1.Client` to use with
+ `requests_oauthlib.OAuth1` instead of the default
+ :param force_include_body: Always include the request body in the
+ signature creation.
+ :param **kwargs: Additional keyword arguments passed to `OAuth1`
+ """
+ super(OAuth1Session, self).__init__()
+ self._client = OAuth1(client_key,
+ client_secret=client_secret,
+ resource_owner_key=resource_owner_key,
+ resource_owner_secret=resource_owner_secret,
+ callback_uri=callback_uri,
+ signature_method=signature_method,
+ signature_type=signature_type,
+ rsa_key=rsa_key,
+ verifier=verifier,
+ client_class=client_class,
+ force_include_body=force_include_body,
+ **kwargs)
+ self.auth = self._client
+
+ @property
+ def authorized(self):
+ """Boolean that indicates whether this session has an OAuth token
+ or not. If `self.authorized` is True, you can reasonably expect
+ OAuth-protected requests to the resource to succeed. If
+ `self.authorized` is False, you need the user to go through the OAuth
+ authentication dance before OAuth-protected requests to the resource
+ will succeed.
+ """
+ if self._client.client.signature_method == SIGNATURE_RSA:
+ # RSA only uses resource_owner_key
+ return bool(self._client.client.resource_owner_key)
+ else:
+ # other methods of authentication use all three pieces
+ return (
+ bool(self._client.client.client_secret) and
+ bool(self._client.client.resource_owner_key) and
+ bool(self._client.client.resource_owner_secret)
+ )
+
+ def authorization_url(self, url, request_token=None, **kwargs):
+ """Create an authorization URL by appending request_token and optional
+ kwargs to url.
+
+ This is the second step in the OAuth 1 workflow. The user should be
+ redirected to this authorization URL, grant access to you, and then
+ be redirected back to you. The redirection back can either be specified
+ during client registration or by supplying a callback URI per request.
+
+ :param url: The authorization endpoint URL.
+ :param request_token: The previously obtained request token.
+ :param kwargs: Optional parameters to append to the URL.
+ :returns: The authorization URL with new parameters embedded.
+
+ An example using a registered default callback URI.
+
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ >>> oauth_session.authorization_url(authorization_url)
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf'
+ >>> oauth_session.authorization_url(authorization_url, foo='bar')
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&foo=bar'
+
+ An example using an explicit callback URI.
+
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret', callback_uri='https://127.0.0.1/callback')
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ >>> oauth_session.authorization_url(authorization_url)
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
+ """
+ kwargs['oauth_token'] = request_token or self._client.client.resource_owner_key
+ log.debug('Adding parameters %s to url %s', kwargs, url)
+ return add_params_to_uri(url, kwargs.items())
+
+ def fetch_request_token(self, url, realm=None):
+ """Fetch a request token.
+
+ This is the first step in the OAuth 1 workflow. A request token is
+ obtained by making a signed post request to url. The token is then
+ parsed from the application/x-www-form-urlencoded response and ready
+ to be used to construct an authorization url.
+
+ :param url: The request token endpoint URL.
+ :param realm: A list of realms to request access to.
+ :returns: The response in dict format.
+
+ Note that a previously set callback_uri will be reset for your
+ convenience, or else signature creation will be incorrect on
+ consecutive requests.
+
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ """
+ self._client.client.realm = ' '.join(realm) if realm else None
+ token = self._fetch_token(url)
+ log.debug('Resetting callback_uri and realm (not needed in next phase).')
+ self._client.client.callback_uri = None
+ self._client.client.realm = None
+ return token
+
+ def fetch_access_token(self, url, verifier=None):
+ """Fetch an access token.
+
+ This is the final step in the OAuth 1 workflow. An access token is
+ obtained using all previously obtained credentials, including the
+ verifier from the authorization step.
+
+ Note that a previously set verifier will be reset for your
+ convenience, or else signature creation will be incorrect on
+ consecutive requests.
+
+ >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
+ >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.parse_authorization_response(redirect_response)
+ {
+ 'oauth_token: 'kjerht2309u',
+ 'oauth_token_secret: 'lsdajfh923874',
+ 'oauth_verifier: 'w34o8967345',
+ }
+ >>> oauth_session.fetch_access_token(access_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ """
+ if verifier:
+ self._client.client.verifier = verifier
+ if not getattr(self._client.client, 'verifier', None):
+ raise VerifierMissing('No client verifier has been set.')
+ token = self._fetch_token(url)
+ log.debug('Resetting verifier attribute, should not be used anymore.')
+ self._client.client.verifier = None
+ return token
+
+ def parse_authorization_response(self, url):
+ """Extract parameters from the post authorization redirect response URL.
+
+ :param url: The full URL that resulted from the user being redirected
+ back from the OAuth provider to you, the client.
+ :returns: A dict of parameters extracted from the URL.
+
+ >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.parse_authorization_response(redirect_response)
+ {
+ 'oauth_token: 'kjerht2309u',
+ 'oauth_token_secret: 'lsdajfh923874',
+ 'oauth_verifier: 'w34o8967345',
+ }
+ """
+ log.debug('Parsing token from query part of url %s', url)
+ token = dict(urldecode(urlparse(url).query))
+ log.debug('Updating internal client token attribute.')
+ self._populate_attributes(token)
+ return token
+
+ def _populate_attributes(self, token):
+ if 'oauth_token' in token:
+ self._client.client.resource_owner_key = token['oauth_token']
+ else:
+ raise TokenMissing(
+ 'Response does not contain a token: {resp}'.format(resp=token),
+ token,
+ )
+ if 'oauth_token_secret' in token:
+ self._client.client.resource_owner_secret = (
+ token['oauth_token_secret'])
+ if 'oauth_verifier' in token:
+ self._client.client.verifier = token['oauth_verifier']
+
+ def _fetch_token(self, url):
+ log.debug('Fetching token from %s using client %s', url, self._client.client)
+ r = self.post(url)
+
+ if r.status_code >= 400:
+ error = "Token request failed with code %s, response was '%s'."
+ raise TokenRequestDenied(error % (r.status_code, r.text), r.status_code)
+
+ log.debug('Decoding token from response "%s"', r.text)
+ try:
+ token = dict(urldecode(r.text))
+ except ValueError as e:
+ error = ("Unable to decode token from token response. "
+ "This is commonly caused by an unsuccessful request where"
+ " a non urlencoded error message is returned. "
+ "The decoding error was %s""" % e)
+ raise ValueError(error)
+
+ log.debug('Obtained token %s', token)
+ log.debug('Updating internal client attributes from token data.')
+ self._populate_attributes(token)
+ return token
+
+ def rebuild_auth(self, prepared_request, response):
+ """
+ When being redirected we should always strip Authorization
+ header, since nonce may not be reused as per OAuth spec.
+ """
+ if 'Authorization' in prepared_request.headers:
+ # If we get redirected to a new host, we should strip out
+ # any authentication headers.
+ prepared_request.headers.pop('Authorization', True)
+ prepared_request.prepare_auth(self.auth)
+ return
--- /dev/null
+from __future__ import unicode_literals
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 import is_secure_transport
+
+
+class OAuth2(object):
+ """Adds proof of authorization (OAuth2 token) to the request."""
+
+ def __init__(self, client_id=None, client=None, token=None):
+ """Construct a new OAuth 2 authorization object.
+
+ :param client_id: Client id obtained during registration
+ :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
+ WebApplicationClient which is useful for any
+ hosted application but not mobile or desktop.
+ :param token: Token dictionary, must include access_token
+ and token_type.
+ """
+ self._client = client or WebApplicationClient(client_id, token=token)
+ if token:
+ for k, v in token.items():
+ setattr(self._client, k, v)
+
+ def __call__(self, r):
+ """Append an OAuth 2 token to the request.
+
+ Note that currently HTTPS is required for all requests. There may be
+ a token type that allows for plain HTTP in the future and then this
+ should be updated to allow plain HTTP on a white list basis.
+ """
+ if not is_secure_transport(r.url):
+ raise InsecureTransportError()
+ r.url, r.headers, r.body = self._client.add_token(r.url,
+ http_method=r.method, body=r.body, headers=r.headers)
+ return r
--- /dev/null
+from __future__ import unicode_literals
+
+import logging
+
+from oauthlib.common import generate_token, urldecode
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
+import requests
+
+log = logging.getLogger(__name__)
+
+
+class TokenUpdated(Warning):
+ def __init__(self, token):
+ super(TokenUpdated, self).__init__()
+ self.token = token
+
+
+class OAuth2Session(requests.Session):
+ """Versatile OAuth 2 extension to :class:`requests.Session`.
+
+ Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec
+ including the four core OAuth 2 grants.
+
+ Can be used to create authorization urls, fetch tokens and access protected
+ resources using the :class:`requests.Session` interface you are used to.
+
+ - :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant
+ - :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant
+ - :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant
+ - :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant
+
+ Note that the only time you will be using Implicit Grant from python is if
+ you are driving a user agent able to obtain URL fragments.
+ """
+
+ def __init__(self, client_id=None, client=None, auto_refresh_url=None,
+ auto_refresh_kwargs=None, scope=None, redirect_uri=None, token=None,
+ state=None, token_updater=None, **kwargs):
+ """Construct a new OAuth 2 client session.
+
+ :param client_id: Client id obtained during registration
+ :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
+ WebApplicationClient which is useful for any
+ hosted application but not mobile or desktop.
+ :param scope: List of scopes you wish to request access to
+ :param redirect_uri: Redirect URI you registered as callback
+ :param token: Token dictionary, must include access_token
+ and token_type.
+ :param state: State string used to prevent CSRF. This will be given
+ when creating the authorization url and must be supplied
+ when parsing the authorization response.
+ Can be either a string or a no argument callable.
+ :auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply
+ this if you wish the client to automatically refresh
+ your access tokens.
+ :auto_refresh_kwargs: Extra arguments to pass to the refresh token
+ endpoint.
+ :token_updater: Method with one argument, token, to be used to update
+ your token databse on automatic token refresh. If not
+ set a TokenUpdated warning will be raised when a token
+ has been refreshed. This warning will carry the token
+ in its token argument.
+ :param kwargs: Arguments to pass to the Session constructor.
+ """
+ super(OAuth2Session, self).__init__(**kwargs)
+ self.client_id = client_id or client.client_id
+ self.scope = scope
+ self.redirect_uri = redirect_uri
+ self.token = token or {}
+ self.state = state or generate_token
+ self._state = state
+ self.auto_refresh_url = auto_refresh_url
+ self.auto_refresh_kwargs = auto_refresh_kwargs or {}
+ self.token_updater = token_updater
+ self._client = client or WebApplicationClient(client_id, token=token)
+ self._client._populate_attributes(token or {})
+
+ # Allow customizations for non compliant providers through various
+ # hooks to adjust requests and responses.
+ self.compliance_hook = {
+ 'access_token_response': set([]),
+ 'refresh_token_response': set([]),
+ 'protected_request': set([]),
+ }
+
+ def new_state(self):
+ """Generates a state string to be used in authorizations."""
+ try:
+ self._state = self.state()
+ log.debug('Generated new state %s.', self._state)
+ except TypeError:
+ self._state = self.state
+ log.debug('Re-using previously supplied state %s.', self._state)
+ return self._state
+
+ @property
+ def authorized(self):
+ """Boolean that indicates whether this session has an OAuth token
+ or not. If `self.authorized` is True, you can reasonably expect
+ OAuth-protected requests to the resource to succeed. If
+ `self.authorized` is False, you need the user to go through the OAuth
+ authentication dance before OAuth-protected requests to the resource
+ will succeed.
+ """
+ return bool(self._client.access_token)
+
+ def authorization_url(self, url, state=None, **kwargs):
+ """Form an authorization URL.
+
+ :param url: Authorization endpoint url, must be HTTPS.
+ :param state: An optional state string for CSRF protection. If not
+ given it will be generated for you.
+ :param kwargs: Extra parameters to include.
+ :return: authorization_url, state
+ """
+ state = state or self.new_state()
+ return self._client.prepare_request_uri(url,
+ redirect_uri=self.redirect_uri,
+ scope=self.scope,
+ state=state,
+ **kwargs), state
+
+ def fetch_token(self, token_url, code=None, authorization_response=None,
+ body='', auth=None, username=None, password=None, method='POST',
+ timeout=None, headers=None, verify=True, **kwargs):
+ """Generic method for fetching an access token from the token endpoint.
+
+ If you are using the MobileApplicationClient you will want to use
+ token_from_fragment instead of fetch_token.
+
+ :param token_url: Token endpoint URL, must use HTTPS.
+ :param code: Authorization code (used by WebApplicationClients).
+ :param authorization_response: Authorization response URL, the callback
+ URL of the request back to you. Used by
+ WebApplicationClients instead of code.
+ :param body: Optional application/x-www-form-urlencoded body to add the
+ include in the token request. Prefer kwargs over body.
+ :param auth: An auth tuple or method as accepted by requests.
+ :param username: Username used by LegacyApplicationClients.
+ :param password: Password used by LegacyApplicationClients.
+ :param method: The HTTP method used to make the request. Defaults
+ to POST, but may also be GET. Other methods should
+ be added as needed.
+ :param headers: Dict to default request headers with.
+ :param timeout: Timeout of the request in seconds.
+ :param verify: Verify SSL certificate.
+ :param kwargs: Extra parameters to include in the token request.
+ :return: A token dict
+ """
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ if not code and authorization_response:
+ self._client.parse_request_uri_response(authorization_response,
+ state=self._state)
+ code = self._client.code
+ elif not code and isinstance(self._client, WebApplicationClient):
+ code = self._client.code
+ if not code:
+ raise ValueError('Please supply either code or '
+ 'authorization_code parameters.')
+
+
+ body = self._client.prepare_request_body(code=code, body=body,
+ redirect_uri=self.redirect_uri, username=username,
+ password=password, **kwargs)
+
+ headers = headers or {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ }
+ if method.upper() == 'POST':
+ r = self.post(token_url, data=dict(urldecode(body)),
+ timeout=timeout, headers=headers, auth=auth,
+ verify=verify)
+ log.debug('Prepared fetch token request body %s', body)
+ elif method.upper() == 'GET':
+ # if method is not 'POST', switch body to querystring and GET
+ r = self.get(token_url, params=dict(urldecode(body)),
+ timeout=timeout, headers=headers, auth=auth,
+ verify=verify)
+ log.debug('Prepared fetch token request querystring %s', body)
+ else:
+ raise ValueError('The method kwarg must be POST or GET.')
+
+ log.debug('Request to fetch token completed with status %s.',
+ r.status_code)
+ log.debug('Request headers were %s', r.request.headers)
+ log.debug('Request body was %s', r.request.body)
+ log.debug('Response headers were %s and content %s.',
+ r.headers, r.text)
+ log.debug('Invoking %d token response hooks.',
+ len(self.compliance_hook['access_token_response']))
+ for hook in self.compliance_hook['access_token_response']:
+ log.debug('Invoking hook %s.', hook)
+ r = hook(r)
+
+ r.raise_for_status()
+
+ self._client.parse_request_body_response(r.text, scope=self.scope)
+ self.token = self._client.token
+ log.debug('Obtained token %s.', self.token)
+ return self.token
+
+ def token_from_fragment(self, authorization_response):
+ """Parse token from the URI fragment, used by MobileApplicationClients.
+
+ :param authorization_response: The full URL of the redirect back to you
+ :return: A token dict
+ """
+ self._client.parse_request_uri_response(authorization_response,
+ state=self._state)
+ self.token = self._client.token
+ return self.token
+
+ def refresh_token(self, token_url, refresh_token=None, body='', auth=None,
+ timeout=None, verify=True, **kwargs):
+ """Fetch a new access token using a refresh token.
+
+ :param token_url: The token endpoint, must be HTTPS.
+ :param refresh_token: The refresh_token to use.
+ :param body: Optional application/x-www-form-urlencoded body to add the
+ include in the token request. Prefer kwargs over body.
+ :param auth: An auth tuple or method as accepted by requests.
+ :param timeout: Timeout of the request in seconds.
+ :param verify: Verify SSL certificate.
+ :param kwargs: Extra parameters to include in the token request.
+ :return: A token dict
+ """
+ if not token_url:
+ raise ValueError('No token endpoint set for auto_refresh.')
+
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ # Need to nullify token to prevent it from being added to the request
+ refresh_token = refresh_token or self.token.get('refresh_token')
+ self.token = {}
+
+ log.debug('Adding auto refresh key word arguments %s.',
+ self.auto_refresh_kwargs)
+ kwargs.update(self.auto_refresh_kwargs)
+ body = self._client.prepare_refresh_body(body=body,
+ refresh_token=refresh_token, scope=self.scope, **kwargs)
+ log.debug('Prepared refresh token request body %s', body)
+ r = self.post(token_url, data=dict(urldecode(body)), auth=auth,
+ timeout=timeout, verify=verify)
+ log.debug('Request to refresh token completed with status %s.',
+ r.status_code)
+ log.debug('Response headers were %s and content %s.',
+ r.headers, r.text)
+ log.debug('Invoking %d token response hooks.',
+ len(self.compliance_hook['refresh_token_response']))
+ for hook in self.compliance_hook['refresh_token_response']:
+ log.debug('Invoking hook %s.', hook)
+ r = hook(r)
+
+ self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
+ if not 'refresh_token' in self.token:
+ log.debug('No new refresh token given. Re-using old.')
+ self.token['refresh_token'] = refresh_token
+ return self.token
+
+ def request(self, method, url, data=None, headers=None, **kwargs):
+ """Intercept all requests and add the OAuth 2 token if present."""
+ if not is_secure_transport(url):
+ raise InsecureTransportError()
+ if self.token:
+ log.debug('Invoking %d protected resource request hooks.',
+ len(self.compliance_hook['protected_request']))
+ for hook in self.compliance_hook['protected_request']:
+ log.debug('Invoking hook %s.', hook)
+ url, headers, data = hook(url, headers, data)
+
+ log.debug('Adding token %s to request.', self.token)
+ try:
+ url, headers, data = self._client.add_token(url,
+ http_method=method, body=data, headers=headers)
+ # Attempt to retrieve and save new access token if expired
+ except TokenExpiredError:
+ if self.auto_refresh_url:
+ log.debug('Auto refresh is set, attempting to refresh at %s.',
+ self.auto_refresh_url)
+ token = self.refresh_token(self.auto_refresh_url)
+ if self.token_updater:
+ log.debug('Updating token to %s using %s.',
+ token, self.token_updater)
+ self.token_updater(token)
+ url, headers, data = self._client.add_token(url,
+ http_method=method, body=data, headers=headers)
+ else:
+ raise TokenUpdated(token)
+ else:
+ raise
+
+ log.debug('Requesting url %s using method %s.', url, method)
+ log.debug('Supplying headers %s and data %s', headers, data)
+ log.debug('Passing through key word arguments %s.', kwargs)
+ return super(OAuth2Session, self).request(method, url,
+ headers=headers, data=data, **kwargs)
+
+ def register_compliance_hook(self, hook_type, hook):
+ """Register a hook for request/response tweaking.
+
+ Available hooks are:
+ access_token_response invoked before token parsing.
+ refresh_token_response invoked before refresh token parsing.
+ protected_request invoked before making a request.
+
+ If you find a new hook is needed please send a GitHub PR request
+ or open an issue.
+ """
+ if hook_type not in self.compliance_hook:
+ raise ValueError('Hook type %s is not in %s.',
+ hook_type, self.compliance_hook)
+ self.compliance_hook[hook_type].add(hook)
"""
Tweepy Twitter API library
"""
-__version__ = '2.3'
+__version__ = '3.3.0'
__author__ = 'Joshua Roesslein'
__license__ = 'MIT'
from tweepy.error import TweepError
from tweepy.api import API
from tweepy.cache import Cache, MemoryCache, FileCache
-from tweepy.auth import OAuthHandler
+from tweepy.auth import OAuthHandler, AppAuthHandler
from tweepy.streaming import Stream, StreamListener
from tweepy.cursor import Cursor
api = API()
def debug(enable=True, level=1):
-
- import httplib
- httplib.HTTPConnection.debuglevel = level
-
+ from six.moves.http_client import HTTPConnection
+ HTTPConnection.debuglevel = level
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
+from __future__ import print_function
+
import os
import mimetypes
+import six
+
from tweepy.binder import bind_api
from tweepy.error import TweepError
-from tweepy.parsers import ModelParser
+from tweepy.parsers import ModelParser, Parser
from tweepy.utils import list_to_csv
"""Twitter API"""
def __init__(self, auth_handler=None,
- host='api.twitter.com', search_host='search.twitter.com',
- cache=None, secure=True, api_root='/1.1', search_root='',
- retry_count=0, retry_delay=0, retry_errors=None, timeout=60,
- parser=None, compression=False):
+ host='api.twitter.com', search_host='search.twitter.com',
+ upload_host='upload.twitter.com', cache=None, api_root='/1.1',
+ search_root='', upload_root='/1.1', retry_count=0,
+ retry_delay=0, retry_errors=None, timeout=60, parser=None,
+ compression=False, wait_on_rate_limit=False,
+ wait_on_rate_limit_notify=False, proxy=''):
+ """ Api instance Constructor
+
+ :param auth_handler:
+ :param host: url of the server of the rest api, default:'api.twitter.com'
+ :param search_host: url of the search server, default:'search.twitter.com'
+ :param upload_host: url of the upload server, default:'upload.twitter.com'
+ :param cache: Cache to query if a GET method is used, default:None
+ :param api_root: suffix of the api version, default:'/1.1'
+ :param search_root: suffix of the search version, default:''
+ :param upload_root: suffix of the upload version, default:'/1.1'
+ :param retry_count: number of allowed retries, default:0
+ :param retry_delay: delay in second between retries, default:0
+ :param retry_errors: default:None
+ :param timeout: delay before to consider the request as timed out in seconds, default:60
+ :param parser: ModelParser instance to parse the responses, default:None
+ :param compression: If the response is compressed, default:False
+ :param wait_on_rate_limit: If the api wait when it hits the rate limit, default:False
+ :param wait_on_rate_limit_notify: If the api print a notification when the rate limit is hit, default:False
+ :param proxy: Url to use as proxy during the HTTP request, default:''
+
+ :raise TypeError: If the given parser is not a ModelParser instance.
+ """
self.auth = auth_handler
self.host = host
self.search_host = search_host
+ self.upload_host = upload_host
self.api_root = api_root
self.search_root = search_root
+ self.upload_root = upload_root
self.cache = cache
- self.secure = secure
self.compression = compression
self.retry_count = retry_count
self.retry_delay = retry_delay
self.retry_errors = retry_errors
self.timeout = timeout
+ self.wait_on_rate_limit = wait_on_rate_limit
+ self.wait_on_rate_limit_notify = wait_on_rate_limit_notify
self.parser = parser or ModelParser()
+ self.proxy = {}
+ if proxy:
+ self.proxy['https'] = proxy
+
+ # Attempt to explain more clearly the parser argument requirements
+ # https://github.com/tweepy/tweepy/issues/421
+ #
+ parser_type = Parser
+ if not isinstance(self.parser, parser_type):
+ raise TypeError(
+ '"parser" argument has to be an instance of "{required}".'
+ ' It is currently a {actual}.'.format(
+ required=parser_type.__name__,
+ actual=type(self.parser)
+ )
+ )
+
+ @property
+ def home_timeline(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/home_timeline
+ :allowed_param:'since_id', 'max_id', 'count'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/home_timeline.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['since_id', 'max_id', 'count'],
+ require_auth=True
+ )
+
+ def statuses_lookup(self, id_, include_entities=None,
+ trim_user=None, map_=None):
+ return self._statuses_lookup(list_to_csv(id_), include_entities,
+ trim_user, map_)
+
+ @property
+ def _statuses_lookup(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/lookup
+ :allowed_param:'id', 'include_entities', 'trim_user', 'map'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/lookup.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['id', 'include_entities', 'trim_user', 'map'],
+ require_auth=True
+ )
+
+ @property
+ def user_timeline(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/user_timeline
+ :allowed_param:'id', 'user_id', 'screen_name', 'since_id'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/user_timeline.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['id', 'user_id', 'screen_name', 'since_id',
+ 'max_id', 'count', 'include_rts']
+ )
+
+ @property
+ def mentions_timeline(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/mentions_timeline
+ :allowed_param:'since_id', 'max_id', 'count'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/mentions_timeline.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['since_id', 'max_id', 'count'],
+ require_auth=True
+ )
+
+ @property
+ def related_results(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/related_results/show/%3id.format
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/related_results/show/{id}.json',
+ payload_type='relation', payload_list=True,
+ allowed_param=['id'],
+ require_auth=False
+ )
+
+ @property
+ def retweets_of_me(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me
+ :allowed_param:'since_id', 'max_id', 'count'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/retweets_of_me.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['since_id', 'max_id', 'count'],
+ require_auth=True
+ )
+
+ @property
+ def get_status(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/show.json',
+ payload_type='status',
+ allowed_param=['id']
+ )
+
+ def update_status(self, media_ids=None, *args, **kwargs):
+ """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update
+ :allowed_param:'status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id', 'display_coordinates', 'media_ids'
+ """
+ post_data = {}
+ if media_ids is not None:
+ post_data["media_ids"] = list_to_csv(media_ids)
+
+ return bind_api(
+ api=self,
+ path='/statuses/update.json',
+ method='POST',
+ payload_type='status',
+ allowed_param=['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id', 'display_coordinates'],
+ require_auth=True
+ )(post_data=post_data, *args, **kwargs)
+
+ def media_upload(self, filename, *args, **kwargs):
+ """ :reference: https://dev.twitter.com/rest/reference/post/media/upload
+ :allowed_param:
+ """
+ f = kwargs.pop('file', None)
+ headers, post_data = API._pack_image(filename, 3072, form_field='media', f=f)
+ kwargs.update({'headers': headers, 'post_data': post_data})
+
+ return bind_api(
+ api=self,
+ path='/media/upload.json',
+ method='POST',
+ payload_type='media',
+ allowed_param=[],
+ require_auth=True,
+ upload_api=True
+ )(*args, **kwargs)
- """ statuses/home_timeline """
- home_timeline = bind_api(
- path = '/statuses/home_timeline.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['since_id', 'max_id', 'count'],
- require_auth = True
- )
-
- """ statuses/user_timeline """
- user_timeline = bind_api(
- path = '/statuses/user_timeline.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['id', 'user_id', 'screen_name', 'since_id',
- 'max_id', 'count', 'include_rts']
- )
-
- """ statuses/mentions """
- mentions_timeline = bind_api(
- path = '/statuses/mentions_timeline.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['since_id', 'max_id', 'count'],
- require_auth = True
- )
-
- """/related_results/show/:id.format"""
- related_results = bind_api(
- path = '/related_results/show/{id}.json',
- payload_type = 'relation', payload_list = True,
- allowed_param = ['id'],
- require_auth = False
- )
-
- """ statuses/retweets_of_me """
- retweets_of_me = bind_api(
- path = '/statuses/retweets_of_me.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['since_id', 'max_id', 'count'],
- require_auth = True
- )
-
- """ statuses/show """
- get_status = bind_api(
- path = '/statuses/show.json',
- payload_type = 'status',
- allowed_param = ['id']
- )
-
- """ statuses/update """
- update_status = bind_api(
- path = '/statuses/update.json',
- method = 'POST',
- payload_type = 'status',
- allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
- require_auth = True
- )
-
- """ statuses/update_with_media """
def update_with_media(self, filename, *args, **kwargs):
- headers, post_data = API._pack_image(filename, 3072, form_field='media[]')
+ """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update_with_media
+ :allowed_param:'status', 'possibly_sensitive', 'in_reply_to_status_id', 'lat', 'long', 'place_id', 'display_coordinates'
+ """
+ f = kwargs.pop('file', None)
+ headers, post_data = API._pack_image(filename, 3072, form_field='media[]', f=f)
kwargs.update({'headers': headers, 'post_data': post_data})
return bind_api(
+ api=self,
path='/statuses/update_with_media.json',
- method = 'POST',
+ method='POST',
payload_type='status',
- allowed_param = [
+ allowed_param=[
'status', 'possibly_sensitive', 'in_reply_to_status_id', 'lat', 'long',
'place_id', 'display_coordinates'
],
require_auth=True
- )(self, *args, **kwargs)
-
- """ statuses/destroy """
- destroy_status = bind_api(
- path = '/statuses/destroy/{id}.json',
- method = 'POST',
- payload_type = 'status',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ statuses/retweet """
- retweet = bind_api(
- path = '/statuses/retweet/{id}.json',
- method = 'POST',
- payload_type = 'status',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ statuses/retweets """
- retweets = bind_api(
- path = '/statuses/retweets/{id}.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['id', 'count'],
- require_auth = True
- )
-
- retweeters = bind_api(
- path = '/statuses/retweeters/ids.json',
- payload_type = 'ids',
- allowed_param = ['id', 'cursor', 'stringify_ids']
- )
-
- """ users/show """
- get_user = bind_api(
- path = '/users/show.json',
- payload_type = 'user',
- allowed_param = ['id', 'user_id', 'screen_name']
- )
-
- ''' statuses/oembed '''
- get_oembed = bind_api(
- path = '/statuses/oembed.json',
- payload_type = 'json',
- allowed_param = ['id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang']
- )
-
- """ Perform bulk look up of users from user ID or screenname """
- def lookup_users(self, user_ids=None, screen_names=None):
- return self._lookup_users(list_to_csv(user_ids), list_to_csv(screen_names))
-
- _lookup_users = bind_api(
- path = '/users/lookup.json',
- payload_type = 'user', payload_list = True,
- allowed_param = ['user_id', 'screen_name'],
- )
-
- """ Get the authenticated user """
+ )(*args, **kwargs)
+
+ @property
+ def destroy_status(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/statuses/destroy/%3Aid
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/destroy/{id}.json',
+ method='POST',
+ payload_type='status',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def retweet(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/statuses/retweet/%3Aid
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/retweet/{id}.json',
+ method='POST',
+ payload_type='status',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def retweets(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweets/%3Aid
+ :allowed_param:'id', 'count'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/retweets/{id}.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['id', 'count'],
+ require_auth=True
+ )
+
+ @property
+ def retweeters(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweeters/ids
+ :allowed_param:'id', 'cursor', 'stringify_ids
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/retweeters/ids.json',
+ payload_type='ids',
+ allowed_param=['id', 'cursor', 'stringify_ids']
+ )
+
+ @property
+ def get_user(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/users/show
+ :allowed_param:'id', 'user_id', 'screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/users/show.json',
+ payload_type='user',
+ allowed_param=['id', 'user_id', 'screen_name']
+ )
+
+ @property
+ def get_oembed(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/statuses/oembed
+ :allowed_param:'id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang'
+ """
+ return bind_api(
+ api=self,
+ path='/statuses/oembed.json',
+ payload_type='json',
+ allowed_param=['id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang']
+ )
+
+ def lookup_users(self, user_ids=None, screen_names=None, include_entities=None):
+ """ Perform bulk look up of users from user ID or screenname """
+ post_data = {}
+ if include_entities is not None:
+ include_entities = 'true' if include_entities else 'false'
+ post_data['include_entities'] = include_entities
+ if user_ids:
+ post_data['user_id'] = list_to_csv(user_ids)
+ if screen_names:
+ post_data['screen_name'] = list_to_csv(screen_names)
+
+ return self._lookup_users(post_data=post_data)
+
+ @property
+ def _lookup_users(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/users/lookup
+ allowed_param='user_id', 'screen_name', 'include_entities'
+ """
+ return bind_api(
+ api=self,
+ path='/users/lookup.json',
+ payload_type='user', payload_list=True,
+ method='POST',
+ )
+
def me(self):
+ """ Get the authenticated user """
return self.get_user(screen_name=self.auth.get_username())
- """ users/search """
- search_users = bind_api(
- path = '/users/search.json',
- payload_type = 'user', payload_list = True,
- require_auth = True,
- allowed_param = ['q', 'count', 'page']
- )
-
- """ users/suggestions/:slug """
- suggested_users = bind_api(
- path = '/users/suggestions/{slug}.json',
- payload_type = 'user', payload_list = True,
- require_auth = True,
- allowed_param = ['slug', 'lang']
- )
-
- """ users/suggestions """
- suggested_categories = bind_api(
- path = '/users/suggestions.json',
- payload_type = 'category', payload_list = True,
- allowed_param = ['lang'],
- require_auth = True
- )
-
- """ users/suggestions/:slug/members """
- suggested_users_tweets = bind_api(
- path = '/users/suggestions/{slug}/members.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['slug'],
- require_auth = True
- )
-
- """ direct_messages """
- direct_messages = bind_api(
- path = '/direct_messages.json',
- payload_type = 'direct_message', payload_list = True,
- allowed_param = ['since_id', 'max_id', 'count'],
- require_auth = True
- )
-
- """ direct_messages/show """
- get_direct_message = bind_api(
- path = '/direct_messages/show/{id}.json',
- payload_type = 'direct_message',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ direct_messages/sent """
- sent_direct_messages = bind_api(
- path = '/direct_messages/sent.json',
- payload_type = 'direct_message', payload_list = True,
- allowed_param = ['since_id', 'max_id', 'count', 'page'],
- require_auth = True
- )
-
- """ direct_messages/new """
- send_direct_message = bind_api(
- path = '/direct_messages/new.json',
- method = 'POST',
- payload_type = 'direct_message',
- allowed_param = ['user', 'screen_name', 'user_id', 'text'],
- require_auth = True
- )
-
- """ direct_messages/destroy """
- destroy_direct_message = bind_api(
- path = '/direct_messages/destroy.json',
- method = 'DELETE',
- payload_type = 'direct_message',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ friendships/create """
- create_friendship = bind_api(
- path = '/friendships/create.json',
- method = 'POST',
- payload_type = 'user',
- allowed_param = ['id', 'user_id', 'screen_name', 'follow'],
- require_auth = True
- )
-
- """ friendships/destroy """
- destroy_friendship = bind_api(
- path = '/friendships/destroy.json',
- method = 'DELETE',
- payload_type = 'user',
- allowed_param = ['id', 'user_id', 'screen_name'],
- require_auth = True
- )
-
- """ friendships/show """
- show_friendship = bind_api(
- path = '/friendships/show.json',
- payload_type = 'friendship',
- allowed_param = ['source_id', 'source_screen_name',
- 'target_id', 'target_screen_name']
- )
-
- """ Perform bulk look up of friendships from user ID or screenname """
+ @property
+ def search_users(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/users/search
+ :allowed_param:'q', 'count', 'page'
+ """
+ return bind_api(
+ api=self,
+ path='/users/search.json',
+ payload_type='user', payload_list=True,
+ require_auth=True,
+ allowed_param=['q', 'count', 'page']
+ )
+
+ @property
+ def suggested_users(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions/%3Aslug
+ :allowed_param:'slug', 'lang'
+ """
+ return bind_api(
+ api=self,
+ path='/users/suggestions/{slug}.json',
+ payload_type='user', payload_list=True,
+ require_auth=True,
+ allowed_param=['slug', 'lang']
+ )
+
+ @property
+ def suggested_categories(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions
+ :allowed_param:'lang'
+ """
+ return bind_api(
+ api=self,
+ path='/users/suggestions.json',
+ payload_type='category', payload_list=True,
+ allowed_param=['lang'],
+ require_auth=True
+ )
+
+ @property
+ def suggested_users_tweets(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions/%3Aslug/members
+ :allowed_param:'slug'
+ """
+ return bind_api(
+ api=self,
+ path='/users/suggestions/{slug}/members.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['slug'],
+ require_auth=True
+ )
+
+ @property
+ def direct_messages(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages
+ :allowed_param:'since_id', 'max_id', 'count'
+ """
+ return bind_api(
+ api=self,
+ path='/direct_messages.json',
+ payload_type='direct_message', payload_list=True,
+ allowed_param=['since_id', 'max_id', 'count'],
+ require_auth=True
+ )
+
+ @property
+ def get_direct_message(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages/show
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/direct_messages/show/{id}.json',
+ payload_type='direct_message',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def sent_direct_messages(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages/sent
+ :allowed_param:'since_id', 'max_id', 'count', 'page'
+ """
+ return bind_api(
+ api=self,
+ path='/direct_messages/sent.json',
+ payload_type='direct_message', payload_list=True,
+ allowed_param=['since_id', 'max_id', 'count', 'page'],
+ require_auth=True
+ )
+
+ @property
+ def send_direct_message(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/direct_messages/new
+ :allowed_param:'user', 'screen_name', 'user_id', 'text'
+ """
+ return bind_api(
+ api=self,
+ path='/direct_messages/new.json',
+ method='POST',
+ payload_type='direct_message',
+ allowed_param=['user', 'screen_name', 'user_id', 'text'],
+ require_auth=True
+ )
+
+ @property
+ def destroy_direct_message(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/direct_messages/destroy
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/direct_messages/destroy.json',
+ method='POST',
+ payload_type='direct_message',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def create_friendship(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/friendships/create
+ :allowed_param:'id', 'user_id', 'screen_name', 'follow'
+ """
+ return bind_api(
+ api=self,
+ path='/friendships/create.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['id', 'user_id', 'screen_name', 'follow'],
+ require_auth=True
+ )
+
+ @property
+ def destroy_friendship(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/friendships/destroy
+ :allowed_param:'id', 'user_id', 'screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/friendships/destroy.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['id', 'user_id', 'screen_name'],
+ require_auth=True
+ )
+
+ @property
+ def show_friendship(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/friendships/show
+ :allowed_param:'source_id', 'source_screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/friendships/show.json',
+ payload_type='friendship',
+ allowed_param=['source_id', 'source_screen_name',
+ 'target_id', 'target_screen_name']
+ )
+
def lookup_friendships(self, user_ids=None, screen_names=None):
+ """ Perform bulk look up of friendships from user ID or screenname """
return self._lookup_friendships(list_to_csv(user_ids), list_to_csv(screen_names))
- _lookup_friendships = bind_api(
- path = '/friendships/lookup.json',
- payload_type = 'relationship', payload_list = True,
- allowed_param = ['user_id', 'screen_name'],
- require_auth = True
- )
-
-
- """ friends/ids """
- friends_ids = bind_api(
- path = '/friends/ids.json',
- payload_type = 'ids',
- allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
- )
-
- """ friends/list """
- friends = bind_api(
- path = '/friends/list.json',
- payload_type = 'user', payload_list = True,
- allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
- )
-
- """ friendships/incoming """
- friendships_incoming = bind_api(
- path = '/friendships/incoming.json',
- payload_type = 'ids',
- allowed_param = ['cursor']
- )
-
- """ friendships/outgoing"""
- friendships_outgoing = bind_api(
- path = '/friendships/outgoing.json',
- payload_type = 'ids',
- allowed_param = ['cursor']
- )
-
- """ followers/ids """
- followers_ids = bind_api(
- path = '/followers/ids.json',
- payload_type = 'ids',
- allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
- )
-
- """ followers/list """
- followers = bind_api(
- path = '/followers/list.json',
- payload_type = 'user', payload_list = True,
- allowed_param = ['id', 'user_id', 'screen_name', 'cursor', 'count',
- 'skip_status', 'include_user_entities']
- )
-
- """ account/verify_credentials """
+ @property
+ def _lookup_friendships(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/friendships/lookup
+ :allowed_param:'user_id', 'screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/friendships/lookup.json',
+ payload_type='relationship', payload_list=True,
+ allowed_param=['user_id', 'screen_name'],
+ require_auth=True
+ )
+
+ @property
+ def friends_ids(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/friends/ids
+ :allowed_param:'id', 'user_id', 'screen_name', 'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/friends/ids.json',
+ payload_type='ids',
+ allowed_param=['id', 'user_id', 'screen_name', 'cursor']
+ )
+
+ @property
+ def friends(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/friends/list
+ :allowed_param:'id', 'user_id', 'screen_name', 'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/friends/list.json',
+ payload_type='user', payload_list=True,
+ allowed_param=['id', 'user_id', 'screen_name', 'cursor']
+ )
+
+ @property
+ def friendships_incoming(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/friendships/incoming
+ :allowed_param:'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/friendships/incoming.json',
+ payload_type='ids',
+ allowed_param=['cursor']
+ )
+
+ @property
+ def friendships_outgoing(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/friendships/outgoing
+ :allowed_param:'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/friendships/outgoing.json',
+ payload_type='ids',
+ allowed_param=['cursor']
+ )
+
+ @property
+ def followers_ids(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/followers/ids
+ :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'count'
+ """
+ return bind_api(
+ api=self,
+ path='/followers/ids.json',
+ payload_type='ids',
+ allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'count']
+ )
+
+ @property
+ def followers(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/followers/list
+ :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'count', 'skip_status', 'include_user_entities'
+ """
+ return bind_api(
+ api=self,
+ path='/followers/list.json',
+ payload_type='user', payload_list=True,
+ allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'count',
+ 'skip_status', 'include_user_entities']
+ )
+
def verify_credentials(self, **kargs):
+ """ :reference: https://dev.twitter.com/rest/reference/get/account/verify_credentials
+ :allowed_param:'include_entities', 'skip_status'
+ """
try:
return bind_api(
- path = '/account/verify_credentials.json',
- payload_type = 'user',
- require_auth = True,
- allowed_param = ['include_entities', 'skip_status'],
- )(self, **kargs)
+ api=self,
+ path='/account/verify_credentials.json',
+ payload_type='user',
+ require_auth=True,
+ allowed_param=['include_entities', 'skip_status'],
+ )(**kargs)
except TweepError as e:
if e.response and e.response.status == 401:
return False
raise
- """ account/rate_limit_status """
- rate_limit_status = bind_api(
- path = '/application/rate_limit_status.json',
- payload_type = 'json',
- allowed_param = ['resources'],
- use_cache = False
- )
-
- """ account/update_delivery_device """
- set_delivery_device = bind_api(
- path = '/account/update_delivery_device.json',
- method = 'POST',
- allowed_param = ['device'],
- payload_type = 'user',
- require_auth = True
- )
-
- """ account/update_profile_colors """
- update_profile_colors = bind_api(
- path = '/account/update_profile_colors.json',
- method = 'POST',
- payload_type = 'user',
- allowed_param = ['profile_background_color', 'profile_text_color',
- 'profile_link_color', 'profile_sidebar_fill_color',
- 'profile_sidebar_border_color'],
- require_auth = True
- )
-
- """ account/update_profile_image """
- def update_profile_image(self, filename):
- headers, post_data = API._pack_image(filename, 700)
- return bind_api(
- path = '/account/update_profile_image.json',
- method = 'POST',
- payload_type = 'user',
- require_auth = True
- )(self, post_data=post_data, headers=headers)
+ @property
+ def rate_limit_status(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/application/rate_limit_status
+ :allowed_param:'resources'
+ """
+ return bind_api(
+ api=self,
+ path='/application/rate_limit_status.json',
+ payload_type='json',
+ allowed_param=['resources'],
+ use_cache=False
+ )
+
+ @property
+ def set_delivery_device(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/account/update_delivery_device
+ :allowed_param:'device'
+ """
+ return bind_api(
+ api=self,
+ path='/account/update_delivery_device.json',
+ method='POST',
+ allowed_param=['device'],
+ payload_type='user',
+ require_auth=True
+ )
+
+ @property
+ def update_profile_colors(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors
+ :allowed_param:'profile_background_color', 'profile_text_color',
+ 'profile_link_color', 'profile_sidebar_fill_color',
+ 'profile_sidebar_border_color'],
+ """
+ return bind_api(
+ api=self,
+ path='/account/update_profile_colors.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['profile_background_color', 'profile_text_color',
+ 'profile_link_color', 'profile_sidebar_fill_color',
+ 'profile_sidebar_border_color'],
+ require_auth=True
+ )
- """ account/update_profile_background_image """
- def update_profile_background_image(self, filename, *args, **kargs):
- headers, post_data = API._pack_image(filename, 800)
- bind_api(
- path = '/account/update_profile_background_image.json',
- method = 'POST',
- payload_type = 'user',
- allowed_param = ['tile'],
- require_auth = True
+ def update_profile_image(self, filename, file_=None):
+ """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_image
+ :allowed_param:'include_entities', 'skip_status'
+ """
+ headers, post_data = API._pack_image(filename, 700, f=file_)
+ return bind_api(
+ api=self,
+ path='/account/update_profile_image.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['include_entities', 'skip_status'],
+ require_auth=True
)(self, post_data=post_data, headers=headers)
- """ account/update_profile_banner """
- def update_profile_banner(self, filename, *args, **kargs):
- headers, post_data = API._pack_image(filename, 700, form_field="banner")
+ def update_profile_background_image(self, filename, **kargs):
+ """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_background_image
+ :allowed_param:'tile', 'include_entities', 'skip_status', 'use'
+ """
+ f = kargs.pop('file', None)
+ headers, post_data = API._pack_image(filename, 800, f=f)
bind_api(
- path = '/account/update_profile_banner.json',
- method = 'POST',
- allowed_param = ['width', 'height', 'offset_left', 'offset_right'],
- require_auth = True
- )(self, post_data=post_data, headers=headers)
+ api=self,
+ path='/account/update_profile_background_image.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['tile', 'include_entities', 'skip_status', 'use'],
+ require_auth=True
+ )(post_data=post_data, headers=headers)
+
+ def update_profile_banner(self, filename, **kargs):
+ """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_banner
+ :allowed_param:'width', 'height', 'offset_left', 'offset_right'
+ """
+ f = kargs.pop('file', None)
+ headers, post_data = API._pack_image(filename, 700, form_field="banner", f=f)
+ bind_api(
+ api=self,
+ path='/account/update_profile_banner.json',
+ method='POST',
+ allowed_param=['width', 'height', 'offset_left', 'offset_right'],
+ require_auth=True
+ )(post_data=post_data, headers=headers)
+
+ @property
+ def update_profile(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile
+ :allowed_param:'name', 'url', 'location', 'description'
+ """
+ return bind_api(
+ api=self,
+ path='/account/update_profile.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['name', 'url', 'location', 'description'],
+ require_auth=True
+ )
+
+ @property
+ def favorites(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/favorites/list
+ :allowed_param:'screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id'
+ """
+ return bind_api(
+ api=self,
+ path='/favorites/list.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id']
+ )
+
+ @property
+ def create_favorite(self):
+ """ :reference:https://dev.twitter.com/rest/reference/post/favorites/create
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/favorites/create.json',
+ method='POST',
+ payload_type='status',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def destroy_favorite(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/favorites/destroy
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/favorites/destroy.json',
+ method='POST',
+ payload_type='status',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def create_block(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/blocks/create
+ :allowed_param:'id', 'user_id', 'screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/blocks/create.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['id', 'user_id', 'screen_name'],
+ require_auth=True
+ )
+
+ @property
+ def destroy_block(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/blocks/destroy
+ :allowed_param:'id', 'user_id', 'screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/blocks/destroy.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['id', 'user_id', 'screen_name'],
+ require_auth=True
+ )
+
+ @property
+ def blocks(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/blocks/list
+ :allowed_param:'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/blocks/list.json',
+ payload_type='user', payload_list=True,
+ allowed_param=['cursor'],
+ require_auth=True
+ )
+ @property
+ def blocks_ids(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/blocks/ids """
+ return bind_api(
+ api=self,
+ path='/blocks/ids.json',
+ payload_type='json',
+ require_auth=True
+ )
+
+ @property
+ def report_spam(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/users/report_spam
+ :allowed_param:'user_id', 'screen_name'
+ """
+ return bind_api(
+ api=self,
+ path='/users/report_spam.json',
+ method='POST',
+ payload_type='user',
+ allowed_param=['user_id', 'screen_name'],
+ require_auth=True
+ )
+
+ @property
+ def saved_searches(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/saved_searches/show/%3Aid """
+ return bind_api(
+ api=self,
+ path='/saved_searches/list.json',
+ payload_type='saved_search', payload_list=True,
+ require_auth=True
+ )
- """ account/update_profile """
- update_profile = bind_api(
- path = '/account/update_profile.json',
- method = 'POST',
- payload_type = 'user',
- allowed_param = ['name', 'url', 'location', 'description'],
- require_auth = True
- )
-
- """ favorites """
- favorites = bind_api(
- path = '/favorites/list.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id']
- )
-
- """ favorites/create """
- create_favorite = bind_api(
- path = '/favorites/create.json',
- method = 'POST',
- payload_type = 'status',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ favorites/destroy """
- destroy_favorite = bind_api(
- path = '/favorites/destroy.json',
- method = 'POST',
- payload_type = 'status',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ blocks/create """
- create_block = bind_api(
- path = '/blocks/create.json',
- method = 'POST',
- payload_type = 'user',
- allowed_param = ['id', 'user_id', 'screen_name'],
- require_auth = True
- )
-
- """ blocks/destroy """
- destroy_block = bind_api(
- path = '/blocks/destroy.json',
- method = 'DELETE',
- payload_type = 'user',
- allowed_param = ['id', 'user_id', 'screen_name'],
- require_auth = True
- )
-
- """ blocks/blocking """
- blocks = bind_api(
- path = '/blocks/list.json',
- payload_type = 'user', payload_list = True,
- allowed_param = ['cursor'],
- require_auth = True
- )
-
- """ blocks/blocking/ids """
- blocks_ids = bind_api(
- path = '/blocks/ids.json',
- payload_type = 'json',
- require_auth = True
- )
-
- """ report_spam """
- report_spam = bind_api(
- path = '/users/report_spam.json',
- method = 'POST',
- payload_type = 'user',
- allowed_param = ['user_id', 'screen_name'],
- require_auth = True
- )
-
- """ saved_searches """
- saved_searches = bind_api(
- path = '/saved_searches/list.json',
- payload_type = 'saved_search', payload_list = True,
- require_auth = True
- )
-
- """ saved_searches/show """
- get_saved_search = bind_api(
- path = '/saved_searches/show/{id}.json',
- payload_type = 'saved_search',
- allowed_param = ['id'],
- require_auth = True
- )
-
- """ saved_searches/create """
- create_saved_search = bind_api(
- path = '/saved_searches/create.json',
- method = 'POST',
- payload_type = 'saved_search',
- allowed_param = ['query'],
- require_auth = True
- )
-
- """ saved_searches/destroy """
- destroy_saved_search = bind_api(
- path = '/saved_searches/destroy/{id}.json',
- method = 'POST',
- payload_type = 'saved_search',
- allowed_param = ['id'],
- require_auth = True
- )
-
- create_list = bind_api(
- path = '/lists/create.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['name', 'mode', 'description'],
- require_auth = True
- )
-
- destroy_list = bind_api(
- path = '/lists/destroy.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['owner_screen_name', 'owner_id', 'list_id', 'slug'],
- require_auth = True
- )
-
- update_list = bind_api(
- path = '/lists/update.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'],
- require_auth = True
- )
-
- lists_all = bind_api(
- path = '/lists/list.json',
- payload_type = 'list', payload_list = True,
- allowed_param = ['screen_name', 'user_id'],
- require_auth = True
- )
-
- lists_memberships = bind_api(
- path = '/lists/memberships.json',
- payload_type = 'list', payload_list = True,
- allowed_param = ['screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'],
- require_auth = True
- )
-
- lists_subscriptions = bind_api(
- path = '/lists/subscriptions.json',
- payload_type = 'list', payload_list = True,
- allowed_param = ['screen_name', 'user_id', 'cursor'],
- require_auth = True
- )
-
- list_timeline = bind_api(
- path = '/lists/statuses.json',
- payload_type = 'status', payload_list = True,
- allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'since_id', 'max_id', 'count', 'include_rts']
- )
-
- get_list = bind_api(
- path = '/lists/show.json',
- payload_type = 'list',
- allowed_param = ['owner_screen_name', 'owner_id', 'slug', 'list_id']
- )
-
- add_list_member = bind_api(
- path = '/lists/members/create.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['screen_name', 'user_id', 'owner_screen_name', 'owner_id', 'slug', 'list_id'],
- require_auth = True
- )
-
- remove_list_member = bind_api(
- path = '/lists/members/destroy.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['screen_name', 'user_id', 'owner_screen_name', 'owner_id', 'slug', 'list_id'],
- require_auth = True
- )
-
- list_members = bind_api(
- path = '/lists/members.json',
- payload_type = 'user', payload_list = True,
- allowed_param = ['owner_screen_name', 'slug', 'list_id', 'owner_id', 'cursor']
- )
-
- show_list_member = bind_api(
- path = '/lists/members/show.json',
- payload_type = 'user',
- allowed_param = ['list_id', 'slug', 'user_id', 'screen_name', 'owner_screen_name', 'owner_id']
- )
-
- subscribe_list = bind_api(
- path = '/lists/subscribers/create.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id'],
- require_auth = True
- )
-
- unsubscribe_list = bind_api(
- path = '/lists/subscribers/destroy.json',
- method = 'POST',
- payload_type = 'list',
- allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id'],
- require_auth = True
- )
-
- list_subscribers = bind_api(
- path = '/lists/subscribers.json',
- payload_type = 'user', payload_list = True,
- allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'cursor']
- )
-
- show_list_subscriber = bind_api(
- path = '/lists/subscribers/show.json',
- payload_type = 'user',
- allowed_param = ['owner_screen_name', 'slug', 'screen_name', 'owner_id', 'list_id', 'user_id']
- )
-
- """ trends/available """
- trends_available = bind_api(
- path = '/trends/available.json',
- payload_type = 'json'
- )
-
- trends_place = bind_api(
- path = '/trends/place.json',
- payload_type = 'json',
- allowed_param = ['id', 'exclude']
- )
-
- trends_closest = bind_api(
- path = '/trends/closest.json',
- payload_type = 'json',
- allowed_param = ['lat', 'long']
- )
-
- """ search """
- search = bind_api(
- path = '/search/tweets.json',
- payload_type = 'search_results',
- allowed_param = ['q', 'lang', 'locale', 'since_id', 'geocode', 'max_id', 'since', 'until', 'result_type', 'count', 'include_entities', 'from', 'to', 'source']
- )
-
- """ trends/daily """
- trends_daily = bind_api(
- path = '/trends/daily.json',
- payload_type = 'json',
- allowed_param = ['date', 'exclude']
- )
-
- """ trends/weekly """
- trends_weekly = bind_api(
- path = '/trends/weekly.json',
- payload_type = 'json',
- allowed_param = ['date', 'exclude']
- )
-
- """ geo/reverse_geocode """
- reverse_geocode = bind_api(
- path = '/geo/reverse_geocode.json',
- payload_type = 'place', payload_list = True,
- allowed_param = ['lat', 'long', 'accuracy', 'granularity', 'max_results']
- )
-
- """ geo/id """
- geo_id = bind_api(
- path = '/geo/id/{id}.json',
- payload_type = 'place',
- allowed_param = ['id']
- )
-
- """ geo/search """
- geo_search = bind_api(
- path = '/geo/search.json',
- payload_type = 'place', payload_list = True,
- allowed_param = ['lat', 'long', 'query', 'ip', 'granularity', 'accuracy', 'max_results', 'contained_within']
- )
-
- """ geo/similar_places """
- geo_similar_places = bind_api(
- path = '/geo/similar_places.json',
- payload_type = 'place', payload_list = True,
- allowed_param = ['lat', 'long', 'name', 'contained_within']
- )
-
- """ help/languages.json """
- supported_languages = bind_api(
- path = '/help/languages.json',
- payload_type = 'json',
- require_auth = True
- )
-
- """ help/configuration """
- configuration = bind_api(
- path = '/help/configuration.json',
- payload_type = 'json',
- require_auth = True
- )
+ @property
+ def get_saved_search(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/saved_searches/show/%3Aid
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/saved_searches/show/{id}.json',
+ payload_type='saved_search',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def create_saved_search(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/saved_searches/create
+ :allowed_param:'query'
+ """
+ return bind_api(
+ api=self,
+ path='/saved_searches/create.json',
+ method='POST',
+ payload_type='saved_search',
+ allowed_param=['query'],
+ require_auth=True
+ )
+
+ @property
+ def destroy_saved_search(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/saved_searches/destroy/%3Aid
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/saved_searches/destroy/{id}.json',
+ method='POST',
+ payload_type='saved_search',
+ allowed_param=['id'],
+ require_auth=True
+ )
+
+ @property
+ def create_list(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/lists/create
+ :allowed_param:'name', 'mode', 'description'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/create.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['name', 'mode', 'description'],
+ require_auth=True
+ )
+
+ @property
+ def destroy_list(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/lists/destroy
+ :allowed_param:'owner_screen_name', 'owner_id', 'list_id', 'slug'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/destroy.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['owner_screen_name', 'owner_id', 'list_id', 'slug'],
+ require_auth=True
+ )
+
+ @property
+ def update_list(self):
+ """ :reference: https://dev.twitter.com/rest/reference/post/lists/update
+ :allowed_param: list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/update.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'],
+ require_auth=True
+ )
+
+ @property
+ def lists_all(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/lists/list
+ :allowed_param:'screen_name', 'user_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/list.json',
+ payload_type='list', payload_list=True,
+ allowed_param=['screen_name', 'user_id'],
+ require_auth=True
+ )
+
+ @property
+ def lists_memberships(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/lists/memberships
+ :allowed_param:'screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/memberships.json',
+ payload_type='list', payload_list=True,
+ allowed_param=['screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'],
+ require_auth=True
+ )
+
+ @property
+ def lists_subscriptions(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/lists/subscriptions
+ :allowed_param:'screen_name', 'user_id', 'cursor'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/subscriptions.json',
+ payload_type='list', payload_list=True,
+ allowed_param=['screen_name', 'user_id', 'cursor'],
+ require_auth=True
+ )
+
+ @property
+ def list_timeline(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/statuses
+ :allowed_param:'owner_screen_name', 'slug', 'owner_id', 'list_id',
+ 'since_id', 'max_id', 'count', 'include_rts
+ """
+ return bind_api(
+ api=self,
+ path='/lists/statuses.json',
+ payload_type='status', payload_list=True,
+ allowed_param=['owner_screen_name', 'slug', 'owner_id',
+ 'list_id', 'since_id', 'max_id', 'count',
+ 'include_rts']
+ )
+
+ @property
+ def get_list(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/lists/show
+ :allowed_param:'owner_screen_name', 'owner_id', 'slug', 'list_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/show.json',
+ payload_type='list',
+ allowed_param=['owner_screen_name', 'owner_id', 'slug', 'list_id']
+ )
+
+ @property
+ def add_list_member(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/create
+ :allowed_param:'screen_name', 'user_id', 'owner_screen_name',
+ 'owner_id', 'slug', 'list_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/members/create.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['screen_name', 'user_id', 'owner_screen_name',
+ 'owner_id', 'slug', 'list_id'],
+ require_auth=True
+ )
+
+ @property
+ def remove_list_member(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy
+ :allowed_param:'screen_name', 'user_id', 'owner_screen_name',
+ 'owner_id', 'slug', 'list_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/members/destroy.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['screen_name', 'user_id', 'owner_screen_name',
+ 'owner_id', 'slug', 'list_id'],
+ require_auth=True
+ )
+
+ def add_list_members(self, screen_name=None, user_id=None, slug=None,
+ list_id=None, owner_id=None, owner_screen_name=None):
+ """ Perform bulk add of list members from user ID or screenname """
+ return self._add_list_members(list_to_csv(screen_name),
+ list_to_csv(user_id),
+ slug, list_id, owner_id,
+ owner_screen_name)
+
+ @property
+ def _add_list_members(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all
+ :allowed_param:'screen_name', 'user_id', 'slug', 'lit_id',
+ 'owner_id', 'owner_screen_name'
+
+ """
+ return bind_api(
+ api=self,
+ path='/lists/members/create_all.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['screen_name', 'user_id', 'slug', 'lit_id',
+ 'owner_id', 'owner_screen_name'],
+ require_auth=True
+ )
+
+ def remove_list_members(self, screen_name=None, user_id=None, slug=None,
+ list_id=None, owner_id=None, owner_screen_name=None):
+ """ Perform bulk remove of list members from user ID or screenname """
+ return self._remove_list_members(list_to_csv(screen_name),
+ list_to_csv(user_id),
+ slug, list_id, owner_id,
+ owner_screen_name)
+
+ @property
+ def _remove_list_members(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all
+ :allowed_param:'screen_name', 'user_id', 'slug', 'lit_id',
+ 'owner_id', 'owner_screen_name'
+
+ """
+ return bind_api(
+ api=self,
+ path='/lists/members/destroy_all.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['screen_name', 'user_id', 'slug', 'lit_id',
+ 'owner_id', 'owner_screen_name'],
+ require_auth=True
+ )
+
+ @property
+ def list_members(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/members
+ :allowed_param:'owner_screen_name', 'slug', 'list_id',
+ 'owner_id', 'cursor
+ """
+ return bind_api(
+ api=self,
+ path='/lists/members.json',
+ payload_type='user', payload_list=True,
+ allowed_param=['owner_screen_name', 'slug', 'list_id',
+ 'owner_id', 'cursor']
+ )
+
+ @property
+ def show_list_member(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/members/show
+ :allowed_param:'list_id', 'slug', 'user_id', 'screen_name',
+ 'owner_screen_name', 'owner_id
+ """
+ return bind_api(
+ api=self,
+ path='/lists/members/show.json',
+ payload_type='user',
+ allowed_param=['list_id', 'slug', 'user_id', 'screen_name',
+ 'owner_screen_name', 'owner_id']
+ )
+
+ @property
+ def subscribe_list(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create
+ :allowed_param:'owner_screen_name', 'slug', 'owner_id',
+ 'list_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/subscribers/create.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['owner_screen_name', 'slug', 'owner_id',
+ 'list_id'],
+ require_auth=True
+ )
+
+ @property
+ def unsubscribe_list(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy
+ :allowed_param:'owner_screen_name', 'slug', 'owner_id',
+ 'list_id'
+ """
+ return bind_api(
+ api=self,
+ path='/lists/subscribers/destroy.json',
+ method='POST',
+ payload_type='list',
+ allowed_param=['owner_screen_name', 'slug', 'owner_id',
+ 'list_id'],
+ require_auth=True
+ )
+
+ @property
+ def list_subscribers(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers
+ :allowed_param:'owner_screen_name', 'slug', 'owner_id',
+ 'list_id', 'cursor
+ """
+ return bind_api(
+ api=self,
+ path='/lists/subscribers.json',
+ payload_type='user', payload_list=True,
+ allowed_param=['owner_screen_name', 'slug', 'owner_id',
+ 'list_id', 'cursor']
+ )
+
+ @property
+ def show_list_subscriber(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show
+ :allowed_param:'owner_screen_name', 'slug', 'screen_name',
+ 'owner_id', 'list_id', 'user_id
+ """
+ return bind_api(
+ api=self,
+ path='/lists/subscribers/show.json',
+ payload_type='user',
+ allowed_param=['owner_screen_name', 'slug', 'screen_name',
+ 'owner_id', 'list_id', 'user_id']
+ )
+
+ @property
+ def trends_available(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/trends/available """
+ return bind_api(
+ api=self,
+ path='/trends/available.json',
+ payload_type='json'
+ )
+
+ @property
+ def trends_place(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/trends/place
+ :allowed_param:'id', 'exclude'
+ """
+ return bind_api(
+ api=self,
+ path='/trends/place.json',
+ payload_type='json',
+ allowed_param=['id', 'exclude']
+ )
+
+ @property
+ def trends_closest(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/trends/closest
+ :allowed_param:'lat', 'long'
+ """
+ return bind_api(
+ api=self,
+ path='/trends/closest.json',
+ payload_type='json',
+ allowed_param=['lat', 'long']
+ )
+
+ @property
+ def search(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/search/tweets
+ :allowed_param:'q', 'lang', 'locale', 'since_id', 'geocode',
+ 'max_id', 'since', 'until', 'result_type', 'count',
+ 'include_entities', 'from', 'to', 'source']
+ """
+ return bind_api(
+ api=self,
+ path='/search/tweets.json',
+ payload_type='search_results',
+ allowed_param=['q', 'lang', 'locale', 'since_id', 'geocode',
+ 'max_id', 'since', 'until', 'result_type',
+ 'count', 'include_entities', 'from',
+ 'to', 'source']
+ )
+
+ @property
+ def reverse_geocode(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/geo/reverse_geocode
+ :allowed_param:'lat', 'long', 'accuracy', 'granularity', 'max_results'
+ """
+ return bind_api(
+ api=self,
+ path='/geo/reverse_geocode.json',
+ payload_type='place', payload_list=True,
+ allowed_param=['lat', 'long', 'accuracy', 'granularity',
+ 'max_results']
+ )
+
+ @property
+ def geo_id(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/geo/id/%3Aplace_id
+ :allowed_param:'id'
+ """
+ return bind_api(
+ api=self,
+ path='/geo/id/{id}.json',
+ payload_type='place',
+ allowed_param=['id']
+ )
+
+ @property
+ def geo_search(self):
+ """ :reference: https://dev.twitter.com/docs/api/1.1/get/geo/search
+ :allowed_param:'lat', 'long', 'query', 'ip', 'granularity',
+ 'accuracy', 'max_results', 'contained_within
+
+ """
+ return bind_api(
+ api=self,
+ path='/geo/search.json',
+ payload_type='place', payload_list=True,
+ allowed_param=['lat', 'long', 'query', 'ip', 'granularity',
+ 'accuracy', 'max_results', 'contained_within']
+ )
+
+ @property
+ def geo_similar_places(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/geo/similar_places
+ :allowed_param:'lat', 'long', 'name', 'contained_within'
+ """
+ return bind_api(
+ api=self,
+ path='/geo/similar_places.json',
+ payload_type='place', payload_list=True,
+ allowed_param=['lat', 'long', 'name', 'contained_within']
+ )
+
+ @property
+ def supported_languages(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/help/languages """
+ return bind_api(
+ api=self,
+ path='/help/languages.json',
+ payload_type='json',
+ require_auth=True
+ )
+
+ @property
+ def configuration(self):
+ """ :reference: https://dev.twitter.com/rest/reference/get/help/configuration """
+ return bind_api(
+ api=self,
+ path='/help/configuration.json',
+ payload_type='json',
+ require_auth=True
+ )
""" Internal use only """
+
@staticmethod
- def _pack_image(filename, max_size, form_field="image"):
+ def _pack_image(filename, max_size, form_field="image", f=None):
"""Pack image from file into multipart-formdata post body"""
# image must be less than 700kb in size
- try:
- if os.path.getsize(filename) > (max_size * 1024):
- raise TweepError('File is too big, must be less than 700kb.')
- except os.error:
- raise TweepError('Unable to access file')
+ if f is None:
+ try:
+ if os.path.getsize(filename) > (max_size * 1024):
+ raise TweepError('File is too big, must be less than %skb.' % max_size)
+ except os.error as e:
+ raise TweepError('Unable to access file: %s' % e.strerror)
+
+ # build the mulitpart-formdata body
+ fp = open(filename, 'rb')
+ else:
+ f.seek(0, 2) # Seek to end of file
+ if f.tell() > (max_size * 1024):
+ raise TweepError('File is too big, must be less than %skb.' % max_size)
+ f.seek(0) # Reset to beginning of file
+ fp = f
# image must be gif, jpeg, or png
file_type = mimetypes.guess_type(filename)
if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
raise TweepError('Invalid file type for image: %s' % file_type)
- # build the mulitpart-formdata body
- fp = open(filename, 'rb')
- BOUNDARY = 'Tw3ePy'
- body = []
- body.append('--' + BOUNDARY)
- body.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (form_field, filename))
- body.append('Content-Type: %s' % file_type)
- body.append('')
+ if isinstance(filename, six.text_type):
+ filename = filename.encode("utf-8")
+
+ BOUNDARY = b'Tw3ePy'
+ body = list()
+ body.append(b'--' + BOUNDARY)
+ body.append('Content-Disposition: form-data; name="{0}";'
+ ' filename="{1}"'.format(form_field, filename)
+ .encode('utf-8'))
+ body.append('Content-Type: {0}'.format(file_type).encode('utf-8'))
+ body.append(b'')
body.append(fp.read())
- body.append('--' + BOUNDARY + '--')
- body.append('')
+ body.append(b'--' + BOUNDARY + b'--')
+ body.append(b'')
fp.close()
- body = '\r\n'.join(body)
+ body = b'\r\n'.join(body)
# build headers
headers = {
}
return headers, body
-
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
+from __future__ import print_function
-from urllib2 import Request, urlopen
-import base64
+import six
+import logging
-from tweepy import oauth
from tweepy.error import TweepError
from tweepy.api import API
+import requests
+from requests_oauthlib import OAuth1Session, OAuth1
+from requests.auth import AuthBase
+from six.moves.urllib.parse import parse_qs
+
+WARNING_MESSAGE = """Warning! Due to a Twitter API bug, signin_with_twitter
+and access_type don't always play nice together. Details
+https://dev.twitter.com/discussions/21281"""
class AuthHandler(object):
class OAuthHandler(AuthHandler):
"""OAuth authentication handler"""
-
OAUTH_HOST = 'api.twitter.com'
OAUTH_ROOT = '/oauth/'
- def __init__(self, consumer_key, consumer_secret, callback=None, secure=True):
- if type(consumer_key) == unicode:
- consumer_key = bytes(consumer_key)
+ def __init__(self, consumer_key, consumer_secret, callback=None):
+ if type(consumer_key) == six.text_type:
+ consumer_key = consumer_key.encode('ascii')
- if type(consumer_secret) == unicode:
- consumer_secret = bytes(consumer_secret)
+ if type(consumer_secret) == six.text_type:
+ consumer_secret = consumer_secret.encode('ascii')
- self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
- self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
- self.request_token = None
+ self.consumer_key = consumer_key
+ self.consumer_secret = consumer_secret
self.access_token = None
+ self.access_token_secret = None
self.callback = callback
self.username = None
- self.secure = secure
+ self.oauth = OAuth1Session(consumer_key,
+ client_secret=consumer_secret,
+ callback_uri=self.callback)
- def _get_oauth_url(self, endpoint, secure=True):
- if self.secure or secure:
- prefix = 'https://'
- else:
- prefix = 'http://'
+ def _get_oauth_url(self, endpoint):
+ return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
- return prefix + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
+ def apply_auth(self):
+ return OAuth1(self.consumer_key,
+ client_secret=self.consumer_secret,
+ resource_owner_key=self.access_token,
+ resource_owner_secret=self.access_token_secret,
+ decoding=None)
- def apply_auth(self, url, method, headers, parameters):
- request = oauth.OAuthRequest.from_consumer_and_token(
- self._consumer, http_url=url, http_method=method,
- token=self.access_token, parameters=parameters
- )
- request.sign_request(self._sigmethod, self._consumer, self.access_token)
- headers.update(request.to_header())
-
- def _get_request_token(self):
+ def _get_request_token(self, access_type=None):
try:
url = self._get_oauth_url('request_token')
- request = oauth.OAuthRequest.from_consumer_and_token(
- self._consumer, http_url=url, callback=self.callback
- )
- request.sign_request(self._sigmethod, self._consumer, None)
- resp = urlopen(Request(url, headers=request.to_header()))
- return oauth.OAuthToken.from_string(resp.read())
+ if access_type:
+ url += '?x_auth_access_type=%s' % access_type
+ return self.oauth.fetch_request_token(url)
except Exception as e:
raise TweepError(e)
- def set_request_token(self, key, secret):
- self.request_token = oauth.OAuthToken(key, secret)
-
def set_access_token(self, key, secret):
- self.access_token = oauth.OAuthToken(key, secret)
+ self.access_token = key
+ self.access_token_secret = secret
- def get_authorization_url(self, signin_with_twitter=False):
+ def get_authorization_url(self,
+ signin_with_twitter=False,
+ access_type=None):
"""Get the authorization URL to redirect the user"""
try:
- # get the request token
- self.request_token = self._get_request_token()
-
- # build auth request and return as url
if signin_with_twitter:
url = self._get_oauth_url('authenticate')
+ if access_type:
+ logging.warning(WARNING_MESSAGE)
else:
url = self._get_oauth_url('authorize')
- request = oauth.OAuthRequest.from_token_and_callback(
- token=self.request_token, http_url=url
- )
-
- return request.to_url()
+ self.request_token = self._get_request_token(access_type=access_type)
+ return self.oauth.authorization_url(url)
except Exception as e:
+ raise
raise TweepError(e)
def get_access_token(self, verifier=None):
"""
try:
url = self._get_oauth_url('access_token')
-
- # build request
- request = oauth.OAuthRequest.from_consumer_and_token(
- self._consumer,
- token=self.request_token, http_url=url,
- verifier=str(verifier)
- )
- request.sign_request(self._sigmethod, self._consumer, self.request_token)
-
- # send request
- resp = urlopen(Request(url, headers=request.to_header()))
- self.access_token = oauth.OAuthToken.from_string(resp.read())
- return self.access_token
+ self.oauth = OAuth1Session(self.consumer_key,
+ client_secret=self.consumer_secret,
+ resource_owner_key=self.request_token['oauth_token'],
+ resource_owner_secret=self.request_token['oauth_token_secret'],
+ verifier=verifier, callback_uri=self.callback)
+ resp = self.oauth.fetch_access_token(url)
+ self.access_token = resp['oauth_token']
+ self.access_token_secret = resp['oauth_token_secret']
+ return self.access_token, self.access_token_secret
except Exception as e:
raise TweepError(e)
and request activation of xAuth for it.
"""
try:
- url = self._get_oauth_url('access_token', secure=True) # must use HTTPS
- request = oauth.OAuthRequest.from_consumer_and_token(
- oauth_consumer=self._consumer,
- http_method='POST', http_url=url,
- parameters = {
- 'x_auth_mode': 'client_auth',
- 'x_auth_username': username,
- 'x_auth_password': password
- }
- )
- request.sign_request(self._sigmethod, self._consumer, None)
-
- resp = urlopen(Request(url, data=request.to_postdata()))
- self.access_token = oauth.OAuthToken.from_string(resp.read())
- return self.access_token
+ url = self._get_oauth_url('access_token')
+ oauth = OAuth1(self.consumer_key,
+ client_secret=self.consumer_secret)
+ r = requests.post(url=url,
+ auth=oauth,
+ headers={'x_auth_mode': 'client_auth',
+ 'x_auth_username': username,
+ 'x_auth_password': password})
+
+ print(r.content)
+ credentials = parse_qs(r.content)
+ return credentials.get('oauth_token')[0], credentials.get('oauth_token_secret')[0]
except Exception as e:
raise TweepError(e)
if user:
self.username = user.screen_name
else:
- raise TweepError("Unable to get username, invalid oauth token!")
+ raise TweepError('Unable to get username,'
+ ' invalid oauth token!')
return self.username
+
+class OAuth2Bearer(AuthBase):
+ def __init__(self, bearer_token):
+ self.bearer_token = bearer_token
+
+ def __call__(self, request):
+ request.headers['Authorization'] = 'Bearer ' + self.bearer_token
+ return request
+
+
+class AppAuthHandler(AuthHandler):
+ """Application-only authentication handler"""
+
+ OAUTH_HOST = 'api.twitter.com'
+ OAUTH_ROOT = '/oauth2/'
+
+ def __init__(self, consumer_key, consumer_secret):
+ self.consumer_key = consumer_key
+ self.consumer_secret = consumer_secret
+ self._bearer_token = ''
+
+ resp = requests.post(self._get_oauth_url('token'),
+ auth=(self.consumer_key,
+ self.consumer_secret),
+ data={'grant_type': 'client_credentials'})
+ data = resp.json()
+ if data.get('token_type') != 'bearer':
+ raise TweepError('Expected token_type to equal "bearer", '
+ 'but got %s instead' % data.get('token_type'))
+
+ self._bearer_token = data['access_token']
+
+ def _get_oauth_url(self, endpoint):
+ return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
+
+ def apply_auth(self):
+ return OAuth2Bearer(self._bearer_token)
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
-import httplib
-import urllib
+from __future__ import print_function
+
import time
import re
-from StringIO import StringIO
-import gzip
+
+from six.moves.urllib.parse import quote
+import requests
+
+import logging
from tweepy.error import TweepError
from tweepy.utils import convert_to_utf8_str
from tweepy.models import Model
+
re_path_template = re.compile('{\w+}')
+log = logging.getLogger('tweepy.binder')
def bind_api(**config):
class APIMethod(object):
+ api = config['api']
path = config['path']
payload_type = config.get('payload_type', None)
payload_list = config.get('payload_list', False)
method = config.get('method', 'GET')
require_auth = config.get('require_auth', False)
search_api = config.get('search_api', False)
+ upload_api = config.get('upload_api', False)
use_cache = config.get('use_cache', True)
+ session = requests.Session()
- def __init__(self, api, args, kargs):
+ def __init__(self, args, kwargs):
+ api = self.api
# If authentication is required and no credentials
# are provided, throw an error.
if self.require_auth and not api.auth:
raise TweepError('Authentication required!')
- self.api = api
- self.post_data = kargs.pop('post_data', None)
- self.retry_count = kargs.pop('retry_count', api.retry_count)
- self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
- self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
- self.headers = kargs.pop('headers', {})
- self.build_parameters(args, kargs)
+ self.post_data = kwargs.pop('post_data', None)
+ self.retry_count = kwargs.pop('retry_count',
+ api.retry_count)
+ self.retry_delay = kwargs.pop('retry_delay',
+ api.retry_delay)
+ self.retry_errors = kwargs.pop('retry_errors',
+ api.retry_errors)
+ self.wait_on_rate_limit = kwargs.pop('wait_on_rate_limit',
+ api.wait_on_rate_limit)
+ self.wait_on_rate_limit_notify = kwargs.pop('wait_on_rate_limit_notify',
+ api.wait_on_rate_limit_notify)
+ self.parser = kwargs.pop('parser', api.parser)
+ self.session.headers = kwargs.pop('headers', {})
+ self.build_parameters(args, kwargs)
# Pick correct URL root to use
if self.search_api:
self.api_root = api.search_root
+ elif self.upload_api:
+ self.api_root = api.upload_root
else:
self.api_root = api.api_root
# Perform any path variable substitution
self.build_path()
- if api.secure:
- self.scheme = 'https://'
- else:
- self.scheme = 'http://'
-
if self.search_api:
self.host = api.search_host
+ elif self.upload_api:
+ self.host = api.upload_host
else:
self.host = api.host
# or older where Host is set including the 443 port.
# This causes Twitter to issue 301 redirect.
# See Issue https://github.com/tweepy/tweepy/issues/12
- self.headers['Host'] = self.host
+ self.session.headers['Host'] = self.host
+ # Monitoring rate limits
+ self._remaining_calls = None
+ self._reset_time = None
- def build_parameters(self, args, kargs):
- self.parameters = {}
+ def build_parameters(self, args, kwargs):
+ self.session.params = {}
for idx, arg in enumerate(args):
if arg is None:
continue
-
try:
- self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
+ self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
except IndexError:
raise TweepError('Too many parameters supplied!')
- for k, arg in kargs.items():
+ for k, arg in kwargs.items():
if arg is None:
continue
- if k in self.parameters:
+ if k in self.session.params:
raise TweepError('Multiple values for parameter %s supplied!' % k)
- self.parameters[k] = convert_to_utf8_str(arg)
+ self.session.params[k] = convert_to_utf8_str(arg)
+
+ log.info("PARAMS: %r", self.session.params)
def build_path(self):
for variable in re_path_template.findall(self.path):
name = variable.strip('{}')
- if name == 'user' and 'user' not in self.parameters and self.api.auth:
+ if name == 'user' and 'user' not in self.session.params and self.api.auth:
# No 'user' parameter provided, fetch it from Auth instead.
value = self.api.auth.get_username()
else:
try:
- value = urllib.quote(self.parameters[name])
+ value = quote(self.session.params[name])
except KeyError:
raise TweepError('No parameter value found for path variable: %s' % name)
- del self.parameters[name]
+ del self.session.params[name]
self.path = self.path.replace(variable, value)
# Build the request URL
url = self.api_root + self.path
- if len(self.parameters):
- url = '%s?%s' % (url, urllib.urlencode(self.parameters))
+ full_url = 'https://' + self.host + url
# Query the cache if one is available
# and this request uses a GET method.
# or maximum number of retries is reached.
retries_performed = 0
while retries_performed < self.retry_count + 1:
- # Open connection
- if self.api.secure:
- conn = httplib.HTTPSConnection(self.host, timeout=self.api.timeout)
- else:
- conn = httplib.HTTPConnection(self.host, timeout=self.api.timeout)
+ # handle running out of api calls
+ if self.wait_on_rate_limit:
+ if self._reset_time is not None:
+ if self._remaining_calls is not None:
+ if self._remaining_calls < 1:
+ sleep_time = self._reset_time - int(time.time())
+ if sleep_time > 0:
+ if self.wait_on_rate_limit_notify:
+ print("Rate limit reached. Sleeping for:", sleep_time)
+ time.sleep(sleep_time + 5) # sleep for few extra sec
+
+ # if self.wait_on_rate_limit and self._reset_time is not None and \
+ # self._remaining_calls is not None and self._remaining_calls < 1:
+ # sleep_time = self._reset_time - int(time.time())
+ # if sleep_time > 0:
+ # if self.wait_on_rate_limit_notify:
+ # print("Rate limit reached. Sleeping for: " + str(sleep_time))
+ # time.sleep(sleep_time + 5) # sleep for few extra sec
# Apply authentication
if self.api.auth:
- self.api.auth.apply_auth(
- self.scheme + self.host + url,
- self.method, self.headers, self.parameters
- )
+ auth = self.api.auth.apply_auth()
# Request compression if configured
if self.api.compression:
- self.headers['Accept-encoding'] = 'gzip'
+ self.session.headers['Accept-encoding'] = 'gzip'
# Execute request
try:
- conn.request(self.method, url, headers=self.headers, body=self.post_data)
- resp = conn.getresponse()
+ resp = self.session.request(self.method,
+ full_url,
+ data=self.post_data,
+ timeout=self.api.timeout,
+ auth=auth,
+ proxies=self.api.proxy)
except Exception as e:
raise TweepError('Failed to send request: %s' % e)
-
+ rem_calls = resp.headers.get('x-rate-limit-remaining')
+ if rem_calls is not None:
+ self._remaining_calls = int(rem_calls)
+ elif isinstance(self._remaining_calls, int):
+ self._remaining_calls -= 1
+ reset_time = resp.headers.get('x-rate-limit-reset')
+ if reset_time is not None:
+ self._reset_time = int(reset_time)
+ if self.wait_on_rate_limit and self._remaining_calls == 0 and (
+ # if ran out of calls before waiting switching retry last call
+ resp.status_code == 429 or resp.status_code == 420):
+ continue
+ retry_delay = self.retry_delay
# Exit request loop if non-retry error code
- if self.retry_errors:
- if resp.status not in self.retry_errors: break
- else:
- if resp.status == 200: break
+ if resp.status_code == 200:
+ break
+ elif (resp.status_code == 429 or resp.status_code == 420) and self.wait_on_rate_limit:
+ if 'retry-after' in resp.headers:
+ retry_delay = float(resp.headers['retry-after'])
+ elif self.retry_errors and resp.status_code not in self.retry_errors:
+ break
# Sleep before retrying request again
- time.sleep(self.retry_delay)
+ time.sleep(retry_delay)
retries_performed += 1
# If an error was returned, throw an exception
self.api.last_response = resp
- if resp.status and not 200 <= resp.status < 300:
+ if resp.status_code and not 200 <= resp.status_code < 300:
try:
- error_msg = self.api.parser.parse_error(resp.read())
+ error_msg = self.parser.parse_error(resp.text)
except Exception:
- error_msg = "Twitter error response: status code = %s" % resp.status
+ error_msg = "Twitter error response: status code = %s" % resp.status_code
raise TweepError(error_msg, resp)
# Parse the response payload
- body = resp.read()
- if resp.getheader('Content-Encoding', '') == 'gzip':
- try:
- zipper = gzip.GzipFile(fileobj=StringIO(body))
- body = zipper.read()
- except Exception as e:
- raise TweepError('Failed to decompress data: %s' % e)
- result = self.api.parser.parse(self, body)
-
- conn.close()
+ result = self.parser.parse(self, resp.text)
# Store result into cache if one is available.
if self.use_cache and self.api.cache and self.method == 'GET' and result:
return result
-
- def _call(api, *args, **kargs):
-
- method = APIMethod(api, args, kargs)
- return method.execute()
-
+ def _call(*args, **kwargs):
+ method = APIMethod(args, kwargs)
+ if kwargs.get('create'):
+ return method
+ else:
+ return method.execute()
# Set pagination mode
if 'cursor' in APIMethod.allowed_param:
_call.pagination_mode = 'cursor'
- elif 'max_id' in APIMethod.allowed_param and \
- 'since_id' in APIMethod.allowed_param:
- _call.pagination_mode = 'id'
+ elif 'max_id' in APIMethod.allowed_param:
+ if 'since_id' in APIMethod.allowed_param:
+ _call.pagination_mode = 'id'
elif 'page' in APIMethod.allowed_param:
_call.pagination_mode = 'page'
return _call
-
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
+from __future__ import print_function
+
import time
import datetime
import threading
def cleanup(self):
self.lock.acquire()
try:
- for k, v in self._entries.items():
+ for k, v in dict(self._entries).items():
if self._is_expired(v, self.timeout):
del self._entries[k]
finally:
def _get_path(self, key):
md5 = hashlib.md5()
- md5.update(key)
+ md5.update(key.encode('utf-8'))
return os.path.join(self.cache_dir, md5.hexdigest())
def _lock_file_dummy(self, path, exclusive=True):
# check if value is expired
if timeout is None:
timeout = self.timeout
- if timeout > 0 and (time.time() - created_time) >= timeout:
- # expired! delete from cache
- value = None
- self._delete_file(path)
+ if timeout > 0:
+ if (time.time() - created_time) >= timeout:
+ # expired! delete from cache
+ value = None
+ self._delete_file(path)
# unlock and return result
self._unlock_file(f_lock)
continue
self._delete_file(os.path.join(self.cache_dir, entry))
+
class MemCacheCache(Cache):
"""Cache interface"""
def get(self, key, timeout=None):
"""Get cached entry if exists and not expired
key: which entry to get
- timeout: override timeout with this value [optional]. DOES NOT WORK HERE
+ timeout: override timeout with this value [optional].
+ DOES NOT WORK HERE
"""
return self.client.get(key)
"""Delete all cached entries. NO-OP"""
raise NotImplementedError
+
class RedisCache(Cache):
- '''Cache running in a redis server'''
+ """Cache running in a redis server"""
- def __init__(self, client, timeout=60, keys_container = 'tweepy:keys', pre_identifier = 'tweepy:'):
+ def __init__(self, client,
+ timeout=60,
+ keys_container='tweepy:keys',
+ pre_identifier='tweepy:'):
Cache.__init__(self, timeout)
self.client = client
self.keys_container = keys_container
return timeout > 0 and (time.time() - entry[0]) >= timeout
def store(self, key, value):
- '''Store the key, value pair in our redis server'''
- # Prepend tweepy to our key, this makes it easier to identify tweepy keys in our redis server
+ """Store the key, value pair in our redis server"""
+ # Prepend tweepy to our key,
+ # this makes it easier to identify tweepy keys in our redis server
key = self.pre_identifier + key
# Get a pipe (to execute several redis commands in one step)
pipe = self.client.pipeline()
pipe.execute()
def get(self, key, timeout=None):
- '''Given a key, returns an element from the redis table'''
+ """Given a key, returns an element from the redis table"""
key = self.pre_identifier + key
# Check to see if we have this key
unpickled_entry = self.client.get(key)
return entry[1]
def count(self):
- '''Note: This is not very efficient, since it retreives all the keys from the redis
- server to know how many keys we have'''
+ """Note: This is not very efficient,
+ since it retreives all the keys from the redis
+ server to know how many keys we have"""
return len(self.client.smembers(self.keys_container))
def delete_entry(self, key):
- '''Delete an object from the redis table'''
+ """Delete an object from the redis table"""
pipe = self.client.pipeline()
pipe.srem(self.keys_container, key)
pipe.delete(key)
pipe.execute()
def cleanup(self):
- '''Cleanup all the expired keys'''
+ """Cleanup all the expired keys"""
keys = self.client.smembers(self.keys_container)
for key in keys:
entry = self.client.get(key)
self.delete_entry(key)
def flush(self):
- '''Delete all entries from the cache'''
+ """Delete all entries from the cache"""
keys = self.client.smembers(self.keys_container)
for key in keys:
self.delete_entry(key)
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
+from __future__ import print_function
+
from tweepy.error import TweepError
+from tweepy.parsers import ModelParser, RawParser
+
class Cursor(object):
"""Pagination helper class"""
i.limit = limit
return i
+
class BaseIterator(object):
def __init__(self, method, args, kargs):
self.kargs = kargs
self.limit = 0
+ def __next__(self):
+ return self.next()
+
def next(self):
raise NotImplementedError
def __iter__(self):
return self
+
class CursorIterator(BaseIterator):
def __init__(self, method, args, kargs):
start_cursor = kargs.pop('cursor', None)
self.next_cursor = start_cursor or -1
self.prev_cursor = start_cursor or 0
- self.count = 0
+ self.num_tweets = 0
def next(self):
- if self.next_cursor == 0 or (self.limit and self.count == self.limit):
+ if self.next_cursor == 0 or (self.limit and self.num_tweets == self.limit):
raise StopIteration
- data, cursors = self.method(
- cursor=self.next_cursor, *self.args, **self.kargs
- )
+ data, cursors = self.method(cursor=self.next_cursor,
+ *self.args,
+ **self.kargs)
self.prev_cursor, self.next_cursor = cursors
if len(data) == 0:
raise StopIteration
- self.count += 1
+ self.num_tweets += 1
return data
def prev(self):
if self.prev_cursor == 0:
raise TweepError('Can not page back more, at first page')
- data, self.next_cursor, self.prev_cursor = self.method(
- cursor=self.prev_cursor, *self.args, **self.kargs
- )
- self.count -= 1
+ data, self.next_cursor, self.prev_cursor = self.method(cursor=self.prev_cursor,
+ *self.args,
+ **self.kargs)
+ self.num_tweets -= 1
return data
+
class IdIterator(BaseIterator):
def __init__(self, method, args, kargs):
BaseIterator.__init__(self, method, args, kargs)
- self.max_id = kargs.get('max_id')
- self.since_id = kargs.get('since_id')
- self.count = 0
+ self.max_id = kargs.pop('max_id', None)
+ self.num_tweets = 0
+ self.results = []
+ self.model_results = []
+ self.index = 0
def next(self):
"""Fetch a set of items with IDs less than current set."""
- if self.limit and self.limit == self.count:
+ if self.limit and self.limit == self.num_tweets:
raise StopIteration
- # max_id is inclusive so decrement by one
- # to avoid requesting duplicate items.
- max_id = self.since_id - 1 if self.max_id else None
- data = self.method(max_id = max_id, *self.args, **self.kargs)
- if len(data) == 0:
+ if self.index >= len(self.results) - 1:
+ data = self.method(max_id=self.max_id, parser=RawParser(), *self.args, **self.kargs)
+
+ if hasattr(self.method, '__self__'):
+ old_parser = self.method.__self__.parser
+ # Hack for models which expect ModelParser to be set
+ self.method.__self__.parser = ModelParser()
+
+ # This is a special invocation that returns the underlying
+ # APIMethod class
+ model = ModelParser().parse(self.method(create=True), data)
+ if hasattr(self.method, '__self__'):
+ self.method.__self__.parser = old_parser
+ result = self.method.__self__.parser.parse(self.method(create=True), data)
+ else:
+ result = model
+
+ if len(self.results) != 0:
+ self.index += 1
+ self.results.append(result)
+ self.model_results.append(model)
+ else:
+ self.index += 1
+ result = self.results[self.index]
+ model = self.model_results[self.index]
+
+ if len(result) == 0:
raise StopIteration
- self.max_id = data.max_id
- self.since_id = data.since_id
- self.count += 1
- return data
+ # TODO: Make this not dependant on the parser making max_id and
+ # since_id available
+ self.max_id = model.max_id
+ self.num_tweets += 1
+ return result
def prev(self):
"""Fetch a set of items with IDs greater than current set."""
- if self.limit and self.limit == self.count:
+ if self.limit and self.limit == self.num_tweets:
raise StopIteration
- since_id = self.max_id
- data = self.method(since_id = since_id, *self.args, **self.kargs)
- if len(data) == 0:
+ self.index -= 1
+ if self.index < 0:
+ # There's no way to fetch a set of tweets directly 'above' the
+ # current set
raise StopIteration
- self.max_id = data.max_id
- self.since_id = data.since_id
- self.count += 1
+
+ data = self.results[self.index]
+ self.max_id = self.model_results[self.index].max_id
+ self.num_tweets += 1
return data
+
class PageIterator(BaseIterator):
def __init__(self, method, args, kargs):
self.current_page = 0
def next(self):
- self.current_page += 1
+ if self.limit > 0:
+ if self.current_page > self.limit:
+ raise StopIteration
+
items = self.method(page=self.current_page, *self.args, **self.kargs)
- if len(items) == 0 or (self.limit > 0 and self.current_page > self.limit):
+ if len(items) == 0:
raise StopIteration
+ self.current_page += 1
return items
def prev(self):
- if (self.current_page == 1):
+ if self.current_page == 1:
raise TweepError('Can not page back more, at first page')
self.current_page -= 1
return self.method(page=self.current_page, *self.args, **self.kargs)
+
class ItemIterator(BaseIterator):
def __init__(self, page_iterator):
self.limit = 0
self.current_page = None
self.page_index = -1
- self.count = 0
+ self.num_tweets = 0
def next(self):
- if self.limit > 0 and self.count == self.limit:
- raise StopIteration
+ if self.limit > 0:
+ if self.num_tweets == self.limit:
+ raise StopIteration
if self.current_page is None or self.page_index == len(self.current_page) - 1:
# Reached end of current page, get the next page...
self.current_page = self.page_iterator.next()
self.page_index = -1
self.page_index += 1
- self.count += 1
+ self.num_tweets += 1
return self.current_page[self.page_index]
def prev(self):
if self.page_index == 0:
raise TweepError('No more items')
self.page_index -= 1
- self.count -= 1
+ self.num_tweets -= 1
return self.current_page[self.page_index]
-
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
+from __future__ import print_function
+
+import six
+
+
class TweepError(Exception):
"""Tweepy exception"""
def __init__(self, reason, response=None):
- self.reason = unicode(reason)
+ self.reason = six.text_type(reason)
self.response = response
Exception.__init__(self, reason)
def __str__(self):
return self.reason
-
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
-from tweepy.error import TweepError
+from __future__ import absolute_import, print_function
+
from tweepy.utils import parse_datetime, parse_html_value, parse_a_href
if self._max_id:
return self._max_id
ids = self.ids()
- return max(ids) if ids else None
+ # Max_id is always set to the *smallest* id, minus one, in the set
+ return (min(ids) - 1) if ids else None
@property
def since_id(self):
if self._since_id:
return self._since_id
ids = self.ids()
- return min(ids) if ids else None
+ # Since_id is always set to the *greatest* id in the set
+ return max(ids) if ids else None
def ids(self):
return [item.id for item in self if hasattr(item, 'id')]
+
class Model(object):
def __init__(self, api=None):
@classmethod
def parse_list(cls, api, json_list):
- """Parse a list of JSON objects into a result set of model instances."""
+ """
+ Parse a list of JSON objects into
+ a result set of model instances.
+ """
results = ResultSet()
for obj in json_list:
if obj:
return results
def __repr__(self):
- state = ['%s=%s' % (k, repr(v)) for (k,v) in vars(self).items()]
+ state = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
return '%s(%s)' % (self.__class__.__name__, ', '.join(state))
@classmethod
def parse(cls, api, json):
status = cls(api)
- for k, v in json.items():
-
- # Hack by guyrt to fix unicode issues, which we do *not* want to teach.
- if isinstance(v, basestring):
- v = v.encode('ascii', errors='ignore')
+ # I'm not proud. Blame billg.
+ json['text'] = str(json['text'].encode('ascii', 'ignore'))[2:-1]
+ json['user']['screen_name'] = str(json['user']['screen_name'].encode('ascii', 'ignore'))[2:-1]
+
+ setattr(status, '_json', json)
+ for k, v in json.items():
if k == 'user':
user_model = getattr(api.parser.model_factory, 'user') if api else User
user = user_model.parse(api, v)
def favorite(self):
return self._api.create_favorite(self.id)
+ def __eq__(self, other):
+ if isinstance(other, Status):
+ return self.id == other.id
+
+ return NotImplemented
+
+ def __ne__(self, other):
+ result = self == other
+
+ if result is NotImplemented:
+ return result
+
+ return not result
+
class User(Model):
@classmethod
def parse(cls, api, json):
user = cls(api)
+ setattr(user, '_json', json)
for k, v in json.items():
if k == 'created_at':
setattr(user, k, parse_datetime(v))
self.following = False
def lists_memberships(self, *args, **kargs):
- return self._api.lists_memberships(user=self.screen_name, *args, **kargs)
+ return self._api.lists_memberships(user=self.screen_name,
+ *args,
+ **kargs)
def lists_subscriptions(self, *args, **kargs):
- return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs)
+ return self._api.lists_subscriptions(user=self.screen_name,
+ *args,
+ **kargs)
def lists(self, *args, **kargs):
- return self._api.lists_all(user=self.screen_name, *args, **kargs)
+ return self._api.lists_all(user=self.screen_name,
+ *args,
+ **kargs)
def followers_ids(self, *args, **kargs):
- return self._api.followers_ids(user_id=self.id, *args, **kargs)
+ return self._api.followers_ids(user_id=self.id,
+ *args,
+ **kargs)
class DirectMessage(Model):
@classmethod
def parse(cls, api, json):
metadata = json['search_metadata']
- results = SearchResults(metadata.get('max_id'), metadata.get('since_id'))
+ results = SearchResults()
results.refresh_url = metadata.get('refresh_url')
results.completed_in = metadata.get('completed_in')
results.query = metadata.get('query')
results.count = metadata.get('count')
results.next_results = metadata.get('next_results')
+ status_model = getattr(api.parser.model_factory, 'status') if api else Status
+
for status in json['statuses']:
- results.append(Status.parse(api, status))
+ results.append(status_model.parse(api, status))
return results
@classmethod
def parse(cls, api, json):
lst = List(api)
- for k,v in json.items():
+ for k, v in json.items():
if k == 'user':
setattr(lst, k, User.parse(api, v))
elif k == 'created_at':
return self._api.destroy_list(self.slug)
def timeline(self, **kargs):
- return self._api.list_timeline(self.user.screen_name, self.slug, **kargs)
+ return self._api.list_timeline(self.user.screen_name,
+ self.slug,
+ **kargs)
def add_member(self, id):
return self._api.add_list_member(self.slug, id)
return self._api.remove_list_member(self.slug, id)
def members(self, **kargs):
- return self._api.list_members(self.user.screen_name, self.slug, **kargs)
+ return self._api.list_members(self.user.screen_name,
+ self.slug,
+ **kargs)
def is_member(self, id):
- return self._api.is_list_member(self.user.screen_name, self.slug, id)
+ return self._api.is_list_member(self.user.screen_name,
+ self.slug,
+ id)
def subscribe(self):
return self._api.subscribe_list(self.user.screen_name, self.slug)
return self._api.unsubscribe_list(self.user.screen_name, self.slug)
def subscribers(self, **kargs):
- return self._api.list_subscribers(self.user.screen_name, self.slug, **kargs)
+ return self._api.list_subscribers(self.user.screen_name,
+ self.slug,
+ **kargs)
def is_subscribed(self, id):
- return self._api.is_subscribed_list(self.user.screen_name, self.slug, id)
+ return self._api.is_subscribed_list(self.user.screen_name,
+ self.slug,
+ id)
+
class Relation(Model):
@classmethod
def parse(cls, api, json):
result = cls(api)
- for k,v in json.items():
+ for k, v in json.items():
if k == 'value' and json['kind'] in ['Tweet', 'LookedupStatus']:
setattr(result, k, Status.parse(api, v))
elif k == 'results':
setattr(result, k, v)
return result
+
class Relationship(Model):
@classmethod
def parse(cls, api, json):
result = cls(api)
- for k,v in json.items():
+ for k, v in json.items():
if k == 'connections':
setattr(result, 'is_following', 'following' in v)
setattr(result, 'is_followed_by', 'followed_by' in v)
setattr(result, k, v)
return result
+
class JSONModel(Model):
@classmethod
results.append(cls.parse(api, obj))
return results
+
+class Media(Model):
+
+ @classmethod
+ def parse(cls, api, json):
+ media = cls(api)
+ for k, v in json.items():
+ setattr(media, k, v)
+ return media
+
+
class ModelFactory(object):
"""
Used by parsers for creating instances
list = List
relation = Relation
relationship = Relationship
+ media = Media
json = JSONModel
ids = IDModel
place = Place
bounding_box = BoundingBox
-
+++ /dev/null
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-
-class OAuthError(RuntimeError):
- """Generic exception class."""
- def __init__(self, message='OAuth error occured.'):
- self.message = message
-
-def build_authenticate_header(realm=''):
- """Optional WWW-Authenticate header (401 error)"""
- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-def escape(s):
- """Escape a URL including any /."""
- return urllib.quote(s, safe='~')
-
-def _utf8_str(s):
- """Convert unicode to utf-8."""
- if isinstance(s, unicode):
- return s.encode("utf-8")
- else:
- return str(s)
-
-def generate_timestamp():
- """Get seconds since epoch (UTC)."""
- return int(time.time())
-
-def generate_nonce(length=8):
- """Generate pseudorandom number."""
- return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-def generate_verifier(length=8):
- """Generate pseudorandom number."""
- return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-class OAuthConsumer(object):
- """Consumer of OAuth authentication.
-
- OAuthConsumer is a data type that represents the identity of the Consumer
- via its shared secret with the Service Provider.
-
- """
- key = None
- secret = None
-
- def __init__(self, key, secret):
- self.key = key
- self.secret = secret
-
-
-class OAuthToken(object):
- """OAuthToken is a data type that represents an End User via either an access
- or request token.
-
- key -- the token
- secret -- the token secret
-
- """
- key = None
- secret = None
- callback = None
- callback_confirmed = None
- verifier = None
-
- def __init__(self, key, secret):
- self.key = key
- self.secret = secret
-
- def set_callback(self, callback):
- self.callback = callback
- self.callback_confirmed = 'true'
-
- def set_verifier(self, verifier=None):
- if verifier is not None:
- self.verifier = verifier
- else:
- self.verifier = generate_verifier()
-
- def get_callback_url(self):
- if self.callback and self.verifier:
- # Append the oauth_verifier.
- parts = urlparse.urlparse(self.callback)
- scheme, netloc, path, params, query, fragment = parts[:6]
- if query:
- query = '%s&oauth_verifier=%s' % (query, self.verifier)
- else:
- query = 'oauth_verifier=%s' % self.verifier
- return urlparse.urlunparse((scheme, netloc, path, params,
- query, fragment))
- return self.callback
-
- def to_string(self):
- data = {
- 'oauth_token': self.key,
- 'oauth_token_secret': self.secret,
- }
- if self.callback_confirmed is not None:
- data['oauth_callback_confirmed'] = self.callback_confirmed
- return urllib.urlencode(data)
-
- def from_string(s):
- """ Returns a token from something like:
- oauth_token_secret=xxx&oauth_token=xxx
- """
- params = cgi.parse_qs(s, keep_blank_values=False)
- key = params['oauth_token'][0]
- secret = params['oauth_token_secret'][0]
- token = OAuthToken(key, secret)
- try:
- token.callback_confirmed = params['oauth_callback_confirmed'][0]
- except KeyError:
- pass # 1.0, no callback confirmed.
- return token
- from_string = staticmethod(from_string)
-
- def __str__(self):
- return self.to_string()
-
-
-class OAuthRequest(object):
- """OAuthRequest represents the request and can be serialized.
-
- OAuth parameters:
- - oauth_consumer_key
- - oauth_token
- - oauth_signature_method
- - oauth_signature
- - oauth_timestamp
- - oauth_nonce
- - oauth_version
- - oauth_verifier
- ... any additional parameters, as defined by the Service Provider.
- """
- parameters = None # OAuth parameters.
- http_method = HTTP_METHOD
- http_url = None
- version = VERSION
-
- def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
- self.http_method = http_method
- self.http_url = http_url
- self.parameters = parameters or {}
-
- def set_parameter(self, parameter, value):
- self.parameters[parameter] = value
-
- def get_parameter(self, parameter):
- try:
- return self.parameters[parameter]
- except:
- raise OAuthError('Parameter not found: %s' % parameter)
-
- def _get_timestamp_nonce(self):
- return self.get_parameter('oauth_timestamp'), self.get_parameter(
- 'oauth_nonce')
-
- def get_nonoauth_parameters(self):
- """Get any non-OAuth parameters."""
- parameters = {}
- for k, v in self.parameters.iteritems():
- # Ignore oauth parameters.
- if k.find('oauth_') < 0:
- parameters[k] = v
- return parameters
-
- def to_header(self, realm=''):
- """Serialize as a header for an HTTPAuth request."""
- auth_header = 'OAuth realm="%s"' % realm
- # Add the oauth parameters.
- if self.parameters:
- for k, v in self.parameters.iteritems():
- if k[:6] == 'oauth_':
- auth_header += ', %s="%s"' % (k, escape(str(v)))
- return {'Authorization': auth_header}
-
- def to_postdata(self):
- """Serialize as post data for a POST request."""
- return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
- for k, v in self.parameters.iteritems()])
-
- def to_url(self):
- """Serialize as a URL for a GET request."""
- return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
-
- def get_normalized_parameters(self):
- """Return a string that contains the parameters that must be signed."""
- params = self.parameters
- try:
- # Exclude the signature if it exists.
- del params['oauth_signature']
- except:
- pass
- # Escape key values before sorting.
- key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
- for k,v in params.items()]
- # Sort lexicographically, first after key, then after value.
- key_values.sort()
- # Combine key value pairs into a string.
- return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
-
- def get_normalized_http_method(self):
- """Uppercases the http method."""
- return self.http_method.upper()
-
- def get_normalized_http_url(self):
- """Parses the URL and rebuilds it to be scheme://host/path."""
- parts = urlparse.urlparse(self.http_url)
- scheme, netloc, path = parts[:3]
- # Exclude default port numbers.
- if scheme == 'http' and netloc[-3:] == ':80':
- netloc = netloc[:-3]
- elif scheme == 'https' and netloc[-4:] == ':443':
- netloc = netloc[:-4]
- return '%s://%s%s' % (scheme, netloc, path)
-
- def sign_request(self, signature_method, consumer, token):
- """Set the signature parameter to the result of build_signature."""
- # Set the signature method.
- self.set_parameter('oauth_signature_method',
- signature_method.get_name())
- # Set the signature.
- self.set_parameter('oauth_signature',
- self.build_signature(signature_method, consumer, token))
-
- def build_signature(self, signature_method, consumer, token):
- """Calls the build signature method within the signature method."""
- return signature_method.build_signature(self, consumer, token)
-
- def from_request(http_method, http_url, headers=None, parameters=None,
- query_string=None):
- """Combines multiple parameter sources."""
- if parameters is None:
- parameters = {}
-
- # Headers
- if headers and 'Authorization' in headers:
- auth_header = headers['Authorization']
- # Check that the authorization header is OAuth.
- if auth_header[:6] == 'OAuth ':
- auth_header = auth_header[6:]
- try:
- # Get the parameters from the header.
- header_params = OAuthRequest._split_header(auth_header)
- parameters.update(header_params)
- except:
- raise OAuthError('Unable to parse OAuth parameters from '
- 'Authorization header.')
-
- # GET or POST query string.
- if query_string:
- query_params = OAuthRequest._split_url_string(query_string)
- parameters.update(query_params)
-
- # URL parameters.
- param_str = urlparse.urlparse(http_url)[4] # query
- url_params = OAuthRequest._split_url_string(param_str)
- parameters.update(url_params)
-
- if parameters:
- return OAuthRequest(http_method, http_url, parameters)
-
- return None
- from_request = staticmethod(from_request)
-
- def from_consumer_and_token(oauth_consumer, token=None,
- callback=None, verifier=None, http_method=HTTP_METHOD,
- http_url=None, parameters=None):
- if not parameters:
- parameters = {}
-
- defaults = {
- 'oauth_consumer_key': oauth_consumer.key,
- 'oauth_timestamp': generate_timestamp(),
- 'oauth_nonce': generate_nonce(),
- 'oauth_version': OAuthRequest.version,
- }
-
- defaults.update(parameters)
- parameters = defaults
-
- if token:
- parameters['oauth_token'] = token.key
- if token.callback:
- parameters['oauth_callback'] = token.callback
- # 1.0a support for verifier.
- if verifier:
- parameters['oauth_verifier'] = verifier
- elif callback:
- # 1.0a support for callback in the request token request.
- parameters['oauth_callback'] = callback
-
- return OAuthRequest(http_method, http_url, parameters)
- from_consumer_and_token = staticmethod(from_consumer_and_token)
-
- def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
- http_url=None, parameters=None):
- if not parameters:
- parameters = {}
-
- parameters['oauth_token'] = token.key
-
- if callback:
- parameters['oauth_callback'] = callback
-
- return OAuthRequest(http_method, http_url, parameters)
- from_token_and_callback = staticmethod(from_token_and_callback)
-
- def _split_header(header):
- """Turn Authorization: header into parameters."""
- params = {}
- parts = header.split(',')
- for param in parts:
- # Ignore realm parameter.
- if param.find('realm') > -1:
- continue
- # Remove whitespace.
- param = param.strip()
- # Split key-value.
- param_parts = param.split('=', 1)
- # Remove quotes and unescape the value.
- params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
- return params
- _split_header = staticmethod(_split_header)
-
- def _split_url_string(param_str):
- """Turn URL string into parameters."""
- parameters = cgi.parse_qs(param_str, keep_blank_values=False)
- for k, v in parameters.iteritems():
- parameters[k] = urllib.unquote(v[0])
- return parameters
- _split_url_string = staticmethod(_split_url_string)
-
-class OAuthServer(object):
- """A worker to check the validity of a request against a data store."""
- timestamp_threshold = 300 # In seconds, five minutes.
- version = VERSION
- signature_methods = None
- data_store = None
-
- def __init__(self, data_store=None, signature_methods=None):
- self.data_store = data_store
- self.signature_methods = signature_methods or {}
-
- def set_data_store(self, data_store):
- self.data_store = data_store
-
- def get_data_store(self):
- return self.data_store
-
- def add_signature_method(self, signature_method):
- self.signature_methods[signature_method.get_name()] = signature_method
- return self.signature_methods
-
- def fetch_request_token(self, oauth_request):
- """Processes a request_token request and returns the
- request token on success.
- """
- try:
- # Get the request token for authorization.
- token = self._get_token(oauth_request, 'request')
- except OAuthError:
- # No token required for the initial token request.
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- try:
- callback = self.get_callback(oauth_request)
- except OAuthError:
- callback = None # 1.0, no callback specified.
- self._check_signature(oauth_request, consumer, None)
- # Fetch a new token.
- token = self.data_store.fetch_request_token(consumer, callback)
- return token
-
- def fetch_access_token(self, oauth_request):
- """Processes an access_token request and returns the
- access token on success.
- """
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- try:
- verifier = self._get_verifier(oauth_request)
- except OAuthError:
- verifier = None
- # Get the request token.
- token = self._get_token(oauth_request, 'request')
- self._check_signature(oauth_request, consumer, token)
- new_token = self.data_store.fetch_access_token(consumer, token, verifier)
- return new_token
-
- def verify_request(self, oauth_request):
- """Verifies an api call and checks all the parameters."""
- # -> consumer and token
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- # Get the access token.
- token = self._get_token(oauth_request, 'access')
- self._check_signature(oauth_request, consumer, token)
- parameters = oauth_request.get_nonoauth_parameters()
- return consumer, token, parameters
-
- def authorize_token(self, token, user):
- """Authorize a request token."""
- return self.data_store.authorize_request_token(token, user)
-
- def get_callback(self, oauth_request):
- """Get the callback URL."""
- return oauth_request.get_parameter('oauth_callback')
-
- def build_authenticate_header(self, realm=''):
- """Optional support for the authenticate header."""
- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
- def _get_version(self, oauth_request):
- """Verify the correct version request for this server."""
- try:
- version = oauth_request.get_parameter('oauth_version')
- except:
- version = VERSION
- if version and version != self.version:
- raise OAuthError('OAuth version %s not supported.' % str(version))
- return version
-
- def _get_signature_method(self, oauth_request):
- """Figure out the signature with some defaults."""
- try:
- signature_method = oauth_request.get_parameter(
- 'oauth_signature_method')
- except:
- signature_method = SIGNATURE_METHOD
- try:
- # Get the signature method object.
- signature_method = self.signature_methods[signature_method]
- except:
- signature_method_names = ', '.join(self.signature_methods.keys())
- raise OAuthError('Signature method %s not supported try one of the '
- 'following: %s' % (signature_method, signature_method_names))
-
- return signature_method
-
- def _get_consumer(self, oauth_request):
- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- consumer = self.data_store.lookup_consumer(consumer_key)
- if not consumer:
- raise OAuthError('Invalid consumer.')
- return consumer
-
- def _get_token(self, oauth_request, token_type='access'):
- """Try to find the token for the provided request token key."""
- token_field = oauth_request.get_parameter('oauth_token')
- token = self.data_store.lookup_token(token_type, token_field)
- if not token:
- raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
- return token
-
- def _get_verifier(self, oauth_request):
- return oauth_request.get_parameter('oauth_verifier')
-
- def _check_signature(self, oauth_request, consumer, token):
- timestamp, nonce = oauth_request._get_timestamp_nonce()
- self._check_timestamp(timestamp)
- self._check_nonce(consumer, token, nonce)
- signature_method = self._get_signature_method(oauth_request)
- try:
- signature = oauth_request.get_parameter('oauth_signature')
- except:
- raise OAuthError('Missing signature.')
- # Validate the signature.
- valid_sig = signature_method.check_signature(oauth_request, consumer,
- token, signature)
- if not valid_sig:
- key, base = signature_method.build_signature_base_string(
- oauth_request, consumer, token)
- raise OAuthError('Invalid signature. Expected signature base '
- 'string: %s' % base)
- built = signature_method.build_signature(oauth_request, consumer, token)
-
- def _check_timestamp(self, timestamp):
- """Verify that timestamp is recentish."""
- timestamp = int(timestamp)
- now = int(time.time())
- lapsed = abs(now - timestamp)
- if lapsed > self.timestamp_threshold:
- raise OAuthError('Expired timestamp: given %d and now %s has a '
- 'greater difference than threshold %d' %
- (timestamp, now, self.timestamp_threshold))
-
- def _check_nonce(self, consumer, token, nonce):
- """Verify that the nonce is uniqueish."""
- nonce = self.data_store.lookup_nonce(consumer, token, nonce)
- if nonce:
- raise OAuthError('Nonce already used: %s' % str(nonce))
-
-
-class OAuthClient(object):
- """OAuthClient is a worker to attempt to execute a request."""
- consumer = None
- token = None
-
- def __init__(self, oauth_consumer, oauth_token):
- self.consumer = oauth_consumer
- self.token = oauth_token
-
- def get_consumer(self):
- return self.consumer
-
- def get_token(self):
- return self.token
-
- def fetch_request_token(self, oauth_request):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def fetch_access_token(self, oauth_request):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def access_resource(self, oauth_request):
- """-> Some protected resource."""
- raise NotImplementedError
-
-
-class OAuthDataStore(object):
- """A database abstraction used to lookup consumers and tokens."""
-
- def lookup_consumer(self, key):
- """-> OAuthConsumer."""
- raise NotImplementedError
-
- def lookup_token(self, oauth_consumer, token_type, token_token):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def fetch_request_token(self, oauth_consumer, oauth_callback):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
- """-> OAuthToken."""
- raise NotImplementedError
-
- def authorize_request_token(self, oauth_token, user):
- """-> OAuthToken."""
- raise NotImplementedError
-
-
-class OAuthSignatureMethod(object):
- """A strategy class that implements a signature method."""
- def get_name(self):
- """-> str."""
- raise NotImplementedError
-
- def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
- """-> str key, str raw."""
- raise NotImplementedError
-
- def build_signature(self, oauth_request, oauth_consumer, oauth_token):
- """-> str."""
- raise NotImplementedError
-
- def check_signature(self, oauth_request, consumer, token, signature):
- built = self.build_signature(oauth_request, consumer, token)
- return built == signature
-
-
-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
-
- def get_name(self):
- return 'HMAC-SHA1'
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- sig = (
- escape(oauth_request.get_normalized_http_method()),
- escape(oauth_request.get_normalized_http_url()),
- escape(oauth_request.get_normalized_parameters()),
- )
-
- key = '%s&' % escape(consumer.secret)
- if token:
- key += escape(token.secret)
- raw = '&'.join(sig)
- return key, raw
-
- def build_signature(self, oauth_request, consumer, token):
- """Builds the base signature string."""
- key, raw = self.build_signature_base_string(oauth_request, consumer,
- token)
-
- # HMAC object.
- try:
- import hashlib # 2.5
- hashed = hmac.new(key, raw, hashlib.sha1)
- except:
- import sha # Deprecated
- hashed = hmac.new(key, raw, sha)
-
- # Calculate the digest base 64.
- return binascii.b2a_base64(hashed.digest())[:-1]
-
-
-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
-
- def get_name(self):
- return 'PLAINTEXT'
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- """Concatenates the consumer key and secret."""
- sig = '%s&' % escape(consumer.secret)
- if token:
- sig = sig + escape(token.secret)
- return sig, sig
-
- def build_signature(self, oauth_request, consumer, token):
- key, raw = self.build_signature_base_string(oauth_request, consumer,
- token)
- return key
\ No newline at end of file
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
+from __future__ import print_function
+
from tweepy.models import ModelFactory
from tweepy.utils import import_simplejson
from tweepy.error import TweepError
except Exception as e:
raise TweepError('Failed to parse JSON payload: %s' % e)
- needsCursors = method.parameters.has_key('cursor')
- if needsCursors and isinstance(json, dict) and 'previous_cursor' in json and 'next_cursor' in json:
- cursors = json['previous_cursor'], json['next_cursor']
- return json, cursors
+ needs_cursors = 'cursor' in method.session.params
+ if needs_cursors and isinstance(json, dict):
+ if 'previous_cursor' in json:
+ if 'next_cursor' in json:
+ cursors = json['previous_cursor'], json['next_cursor']
+ return json, cursors
else:
return json
def parse(self, method, payload):
try:
- if method.payload_type is None: return
+ if method.payload_type is None:
+ return
model = getattr(self.model_factory, method.payload_type)
except AttributeError:
- raise TweepError('No model for this payload type: %s' % method.payload_type)
+ raise TweepError('No model for this payload type: '
+ '%s' % method.payload_type)
json = JSONParser.parse(self, method, payload)
if isinstance(json, tuple):
return result, cursors
else:
return result
-
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
+# Appengine users: https://developers.google.com/appengine/docs/python/sockets/#making_httplib_use_sockets
+
+from __future__ import absolute_import, print_function
+
import logging
-import httplib
-from socket import timeout
+import requests
+from requests.exceptions import Timeout
from threading import Thread
from time import sleep
+
+import six
+
import ssl
from tweepy.models import Status
from tweepy.api import API
from tweepy.error import TweepError
-from tweepy.utils import import_simplejson, urlencode_noplus
+from tweepy.utils import import_simplejson
json = import_simplejson()
STREAM_VERSION = '1.1'
status = Status.parse(self.api, data)
if self.on_direct_message(status) is False:
return False
+ elif 'friends' in data:
+ if self.on_friends(data['friends']) is False:
+ return False
elif 'limit' in data:
if self.on_limit(data['limit']['track']) is False:
return False
elif 'disconnect' in data:
if self.on_disconnect(data['disconnect']) is False:
return False
+ elif 'warning' in data:
+ if self.on_warning(data['warning']) is False:
+ return False
else:
logging.error("Unknown message type: " + str(raw_data))
+ def keep_alive(self):
+ """Called when a keep-alive arrived"""
+ return
+
def on_status(self, status):
"""Called when a new status arrives"""
return
"""Called when a new direct message arrives"""
return
+ def on_friends(self, friends):
+ """Called when a friends list arrives.
+
+ friends is a list that contains user_id
+ """
+ return
+
def on_limit(self, track):
- """Called when a limitation notice arrvies"""
+ """Called when a limitation notice arrives"""
return
def on_error(self, status_code):
"""
return
+ def on_warning(self, notice):
+ """Called when a disconnection warning message arrives"""
+ return
+
+
+class ReadBuffer(object):
+ """Buffer data from the response in a smarter way than httplib/requests can.
+
+ Tweets are roughly in the 2-12kb range, averaging around 3kb.
+ Requests/urllib3/httplib/socket all use socket.read, which blocks
+ until enough data is returned. On some systems (eg google appengine), socket
+ reads are quite slow. To combat this latency we can read big chunks,
+ but the blocking part means we won't get results until enough tweets
+ have arrived. That may not be a big deal for high throughput systems.
+ For low throughput systems we don't want to sacrafice latency, so we
+ use small chunks so it can read the length and the tweet in 2 read calls.
+ """
+
+ def __init__(self, stream, chunk_size):
+ self._stream = stream
+ self._buffer = u""
+ self._chunk_size = chunk_size
+
+ def read_len(self, length):
+ while not self._stream.closed:
+ if len(self._buffer) >= length:
+ return self._pop(length)
+ read_len = max(self._chunk_size, length - len(self._buffer))
+ self._buffer += self._stream.read(read_len).decode("ascii")
+
+ def read_line(self, sep='\n'):
+ start = 0
+ while not self._stream.closed:
+ loc = self._buffer.find(sep, start)
+ if loc >= 0:
+ return self._pop(loc + len(sep))
+ else:
+ start = len(self._buffer)
+ self._buffer += self._stream.read(self._chunk_size).decode("ascii")
+
+ def _pop(self, length):
+ r = self._buffer[:length]
+ self._buffer = self._buffer[length:]
+ return r
+
class Stream(object):
self.running = False
self.timeout = options.get("timeout", 300.0)
self.retry_count = options.get("retry_count")
- # values according to https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
+ # values according to
+ # https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
self.retry_time_start = options.get("retry_time", 5.0)
self.retry_420_start = options.get("retry_420", 60.0)
self.retry_time_cap = options.get("retry_time_cap", 320.0)
self.snooze_time_step = options.get("snooze_time", 0.25)
self.snooze_time_cap = options.get("snooze_time_cap", 16)
- self.buffer_size = options.get("buffer_size", 1500)
- if options.get("secure", True):
- self.scheme = "https"
- else:
- self.scheme = "http"
+
+ # The default socket.read size. Default to less than half the size of
+ # a tweet so that it reads tweets with the minimal latency of 2 reads
+ # per tweet. Values higher than ~1kb will increase latency by waiting
+ # for more data to arrive but may also increase throughput by doing
+ # fewer socket read calls.
+ self.chunk_size = options.get("chunk_size", 512)
+
+ self.verify = options.get("verify", True)
self.api = API()
self.headers = options.get("headers") or {}
- self.parameters = None
+ self.new_session()
self.body = None
self.retry_time = self.retry_time_start
self.snooze_time = self.snooze_time_step
+ def new_session(self):
+ self.session = requests.Session()
+ self.session.headers = self.headers
+ self.session.params = None
+
def _run(self):
# Authenticate
- url = "%s://%s%s" % (self.scheme, self.host, self.url)
+ url = "https://%s%s" % (self.host, self.url)
# Connect and process the stream
error_counter = 0
- conn = None
+ resp = None
exception = None
while self.running:
- if self.retry_count is not None and error_counter > self.retry_count:
- # quit if error count greater than retry count
- break
+ if self.retry_count is not None:
+ if error_counter > self.retry_count:
+ # quit if error count greater than retry count
+ break
try:
- if self.scheme == "http":
- conn = httplib.HTTPConnection(self.host, timeout=self.timeout)
- else:
- conn = httplib.HTTPSConnection(self.host, timeout=self.timeout)
- self.auth.apply_auth(url, 'POST', self.headers, self.parameters)
- conn.connect()
- conn.request('POST', self.url, self.body, headers=self.headers)
- resp = conn.getresponse()
- if resp.status != 200:
- if self.listener.on_error(resp.status) is False:
+ auth = self.auth.apply_auth()
+ resp = self.session.request('POST',
+ url,
+ data=self.body,
+ timeout=self.timeout,
+ stream=True,
+ auth=auth,
+ verify=self.verify)
+ if resp.status_code != 200:
+ if self.listener.on_error(resp.status_code) is False:
break
error_counter += 1
- if resp.status == 420:
- self.retry_time = max(self.retry_420_start, self.retry_time)
+ if resp.status_code == 420:
+ self.retry_time = max(self.retry_420_start,
+ self.retry_time)
sleep(self.retry_time)
- self.retry_time = min(self.retry_time * 2, self.retry_time_cap)
+ self.retry_time = min(self.retry_time * 2,
+ self.retry_time_cap)
else:
error_counter = 0
self.retry_time = self.retry_time_start
self.snooze_time = self.snooze_time_step
self.listener.on_connect()
self._read_loop(resp)
- except (timeout, ssl.SSLError) as exc:
+ except (Timeout, ssl.SSLError) as exc:
+ # This is still necessary, as a SSLError can actually be
+ # thrown when using Requests
# If it's not time out treat it like any other exception
- if isinstance(exc, ssl.SSLError) and not (exc.args and 'timed out' in str(exc.args[0])):
- exception = exc
- break
-
- if self.listener.on_timeout() == False:
+ if isinstance(exc, ssl.SSLError):
+ if not (exc.args and 'timed out' in str(exc.args[0])):
+ exception = exc
+ break
+ if self.listener.on_timeout() is False:
break
if self.running is False:
break
- conn.close()
sleep(self.snooze_time)
self.snooze_time = min(self.snooze_time + self.snooze_time_step,
self.snooze_time_cap)
- except Exception as exception:
- # any other exception is fatal, so kill loop
- break
+ # except Exception as exc:
+ # exception = exc
+ # # any other exception is fatal, so kill loop
+ # break
# cleanup
self.running = False
- if conn:
- conn.close()
+ if resp:
+ resp.close()
+
+ self.new_session()
if exception:
# call a handler first so that the exception can be logged.
self.running = False
def _read_loop(self, resp):
+ buf = ReadBuffer(resp.raw, self.chunk_size)
+
+ while self.running and not resp.raw.closed:
+ length = 0
+ while not resp.raw.closed:
+ line = buf.read_line().strip()
+ if not line:
+ self.listener.keep_alive() # keep-alive new lines are expected
+ elif line.isdigit():
+ length = int(line)
+ break
+ else:
+ raise TweepError('Expecting length, unexpected value found')
- while self.running and not resp.isclosed():
-
- # Note: keep-alive newlines might be inserted before each length value.
- # read until we get a digit...
- c = '\n'
- while c == '\n' and self.running and not resp.isclosed():
- c = resp.read(1)
- delimited_string = c
-
- # read rest of delimiter length..
- d = ''
- while d != '\n' and self.running and not resp.isclosed():
- d = resp.read(1)
- delimited_string += d
-
- # read the next twitter status object
- if delimited_string.strip().isdigit():
- next_status_obj = resp.read( int(delimited_string) )
+ next_status_obj = buf.read_len(length)
+ if self.running:
self._data(next_status_obj)
- if resp.isclosed():
+ # # Note: keep-alive newlines might be inserted before each length value.
+ # # read until we get a digit...
+ # c = b'\n'
+ # for c in resp.iter_content(decode_unicode=True):
+ # if c == b'\n':
+ # continue
+ # break
+ #
+ # delimited_string = c
+ #
+ # # read rest of delimiter length..
+ # d = b''
+ # for d in resp.iter_content(decode_unicode=True):
+ # if d != b'\n':
+ # delimited_string += d
+ # continue
+ # break
+ #
+ # # read the next twitter status object
+ # if delimited_string.decode('utf-8').strip().isdigit():
+ # status_id = int(delimited_string)
+ # next_status_obj = resp.raw.read(status_id)
+ # if self.running:
+ # self._data(next_status_obj.decode('utf-8'))
+
+
+ if resp.raw.closed:
self.on_closed(resp)
def _start(self, async):
self.running = True
if async:
- Thread(target=self._run).start()
+ self._thread = Thread(target=self._run)
+ self._thread.start()
else:
self._run()
""" Called when the response has been closed by Twitter """
pass
- def userstream(self, stall_warnings=False, _with=None, replies=None,
- track=None, locations=None, async=False, encoding='utf8'):
- self.parameters = {'delimited': 'length'}
+ def userstream(self,
+ stall_warnings=False,
+ _with=None,
+ replies=None,
+ track=None,
+ locations=None,
+ async=False,
+ encoding='utf8'):
+ self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
- self.url = '/%s/user.json?delimited=length' % STREAM_VERSION
- self.host='userstream.twitter.com'
+ self.url = '/%s/user.json' % STREAM_VERSION
+ self.host = 'userstream.twitter.com'
if stall_warnings:
- self.parameters['stall_warnings'] = stall_warnings
+ self.session.params['stall_warnings'] = stall_warnings
if _with:
- self.parameters['with'] = _with
+ self.session.params['with'] = _with
if replies:
- self.parameters['replies'] = replies
+ self.session.params['replies'] = replies
if locations and len(locations) > 0:
- assert len(locations) % 4 == 0
- self.parameters['locations'] = ','.join(['%.2f' % l for l in locations])
+ if len(locations) % 4 != 0:
+ raise TweepError("Wrong number of locations points, "
+ "it has to be a multiple of 4")
+ self.session.params['locations'] = ','.join(['%.2f' % l for l in locations])
if track:
- encoded_track = [s.encode(encoding) for s in track]
- self.parameters['track'] = ','.join(encoded_track)
- self.body = urlencode_noplus(self.parameters)
+ self.session.params['track'] = u','.join(track).encode(encoding)
+
self._start(async)
def firehose(self, count=None, async=False):
- self.parameters = {'delimited': 'length'}
+ self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/firehose.json?delimited=length' % STREAM_VERSION
+ self.url = '/%s/statuses/firehose.json' % STREAM_VERSION
if count:
self.url += '&count=%s' % count
self._start(async)
def retweet(self, async=False):
- self.parameters = {'delimited': 'length'}
+ self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/retweet.json?delimited=length' % STREAM_VERSION
+ self.url = '/%s/statuses/retweet.json' % STREAM_VERSION
self._start(async)
- def sample(self, count=None, async=False):
- self.parameters = {'delimited': 'length'}
+ def sample(self, async=False, languages=None):
+ self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/sample.json?delimited=length' % STREAM_VERSION
- if count:
- self.url += '&count=%s' % count
+ self.url = '/%s/statuses/sample.json' % STREAM_VERSION
+ if languages:
+ self.session.params['language'] = ','.join(map(str, languages))
self._start(async)
def filter(self, follow=None, track=None, async=False, locations=None,
- count=None, stall_warnings=False, languages=None, encoding='utf8'):
- self.parameters = {}
- self.headers['Content-type'] = "application/x-www-form-urlencoded"
+ stall_warnings=False, languages=None, encoding='utf8'):
+ self.body = {}
+ self.session.headers['Content-type'] = "application/x-www-form-urlencoded"
if self.running:
raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/filter.json?delimited=length' % STREAM_VERSION
+ self.url = '/%s/statuses/filter.json' % STREAM_VERSION
if follow:
- encoded_follow = [s.encode(encoding) for s in follow]
- self.parameters['follow'] = ','.join(encoded_follow)
+ self.body['follow'] = u','.join(follow).encode(encoding)
if track:
- encoded_track = [s.encode(encoding) for s in track]
- self.parameters['track'] = ','.join(encoded_track)
+ self.body['track'] = u','.join(track).encode(encoding)
if locations and len(locations) > 0:
- assert len(locations) % 4 == 0
- self.parameters['locations'] = ','.join(['%.4f' % l for l in locations])
- if count:
- self.parameters['count'] = count
+ if len(locations) % 4 != 0:
+ raise TweepError("Wrong number of locations points, "
+ "it has to be a multiple of 4")
+ self.body['locations'] = u','.join(['%.4f' % l for l in locations])
if stall_warnings:
- self.parameters['stall_warnings'] = stall_warnings
+ self.body['stall_warnings'] = stall_warnings
if languages:
- self.parameters['language'] = ','.join(map(str, languages))
- self.body = urlencode_noplus(self.parameters)
- self.parameters['delimited'] = 'length'
+ self.body['language'] = u','.join(map(str, languages))
+ self.session.params = {'delimited': 'length'}
+ self.host = 'stream.twitter.com'
+ self._start(async)
+
+ def sitestream(self, follow, stall_warnings=False,
+ with_='user', replies=False, async=False):
+ self.body = {}
+ if self.running:
+ raise TweepError('Stream object already connected!')
+ self.url = '/%s/site.json' % STREAM_VERSION
+ self.body['follow'] = u','.join(map(six.text_type, follow))
+ self.body['delimited'] = 'length'
+ if stall_warnings:
+ self.body['stall_warnings'] = stall_warnings
+ if with_:
+ self.body['with'] = with_
+ if replies:
+ self.body['replies'] = replies
self._start(async)
def disconnect(self):
if self.running is False:
return
self.running = False
-
# Copyright 2010 Joshua Roesslein
# See LICENSE for details.
+from __future__ import print_function
+
from datetime import datetime
-import time
-import re
-import locale
-from urllib import quote
+
+import six
+from six.moves.urllib.parse import quote
+
from email.utils import parsedate
def convert_to_utf8_str(arg):
# written by Michael Norton (http://docondev.blogspot.com/)
- if isinstance(arg, unicode):
+ if isinstance(arg, six.text_type):
arg = arg.encode('utf-8')
- elif not isinstance(arg, str):
- arg = str(arg)
+ elif not isinstance(arg, bytes):
+ arg = six.text_type(arg).encode('utf-8')
return arg
-
def import_simplejson():
try:
import simplejson as json
import json # Python 2.6+
except ImportError:
try:
- from django.utils import simplejson as json # Google App Engine
+ # Google App Engine
+ from django.utils import simplejson as json
except ImportError:
raise ImportError("Can't load a json library")
return json
+
def list_to_csv(item_list):
if item_list:
return ','.join([str(i) for i in item_list])
-
-def urlencode_noplus(query):
- return '&'.join(['%s=%s' % (quote(str(k), ''), quote(str(v), '')) \
- for k, v in query.iteritems()])
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
-api = tweepy.API(auth, parser=tweepy.parsers.RawParser)
+api = tweepy.API(auth, parser=tweepy.parsers.RawParser())
@classmethod
def parse(cls, api, raw):
class StreamListener(tweepy.StreamListener):
def on_status(self, tweet):
- print tweet.json
+ print(tweet.json)
def on_error(self, status_code):
- print 'Error: ' + repr(status_code)
+ print('Error: ' + repr(status_code))
return False
l = StreamListener()
class StreamListener(tweepy.StreamListener):
def on_status(self, tweet):
- print tweet.user.screen_name + "\t" + tweet.text
+ print(tweet.author.screen_name + "\t" + tweet.text)
def on_error(self, status_code):
- print 'Error: ' + repr(status_code)
+ print('Error: ' + repr(status_code))
return False
l = StreamListener()
class StreamListener(tweepy.StreamListener):
def on_status(self, tweet):
- print tweet.user.screen_name + "\t" + tweet.text
+ print(tweet.author.screen_name + "\t" + tweet.text)
def on_error(self, status_code):
- print 'Error: ' + repr(status_code)
+ print( 'Error: ' + repr(status_code))
return False
l = StreamListener()
public_tweets = api.home_timeline(count=100)
for tweet in public_tweets:
- print tweet.text
+ print(tweet.text)
user = api.get_user('makoshark')
-print user.screen_name + " has " + str(user.followers_count) + " followers."
+print(user.screen_name + " has " + str(user.followers_count) + " followers.")
-print "They include these 100 people:"
+print("They include these 100 people:")
for friend in user.friends(count=100):
- print friend.screen_name
+ print(friend.screen_name)
public_tweets = api.search("data science", count=20)
for tweet in public_tweets:
- print tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text
+ print(tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text)
public_tweets = api.search("data science", count=10)
for tweet in public_tweets:
- print >>output_file, tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text
+ print(tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text, file=output_file)