├── README.md └── examples └── test_client ├── test.py ├── test_oauth_client.py └── oauth.py /README.md: -------------------------------------------------------------------------------- 1 | # Khan Academy API Documentation and Examples 2 | 3 | ## Documentation 4 | 5 | * [Full API documentation](https://github.com/Khan/khan-api/wiki/Khan-Academy-API) is found in the wiki. 6 | 7 | ## Examples 8 | 9 | All example code is [MIT licensed](http://en.wikipedia.org/wiki/MIT_License). 10 | 11 | * There's a [small test client](https://github.com/Khan/khan-api/tree/master/examples/test_client ) in this repo that can be used to test your API calls or to serve as an example for implementing [the Khan Academy authentication flow](https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication). 12 | 13 | * To use the test client: 14 | 15 | 1. `python test.py` 16 | 2. Enter your consumer key and secret 17 | 3. Enter the Khan Academy url you want to test against (unless you're running a local version, this'll be `http://www.khanacademy.org`) 18 | 4. A browser will pop up for you to walk through the authentication flow. When done, it will redirect you to a page that says "Ok" and in the URL there will be two parameter values you need: `oauth_token` and `oauth_token_secret`. 19 | 5. Go back to the test.py command line and enter your new token and secret. 20 | 6. You'll now have a loop that lets you run Khan Academy API queries, authenticated as the user you logged in as. 21 | 7. Example: 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/test_client/test.py: -------------------------------------------------------------------------------- 1 | from test_oauth_client import TestOAuthClient 2 | from oauth import OAuthToken 3 | 4 | # This is a quick, gross little interactive script for testing our OAuth API. 5 | 6 | CONSUMER_KEY = "" 7 | CONSUMER_SECRET = "" 8 | SERVER_URL = "" # http://local.kamenstestapp.appspot.com:8084 9 | 10 | REQUEST_TOKEN = None 11 | ACCESS_TOKEN = None 12 | 13 | def get_request_token(): 14 | global REQUEST_TOKEN 15 | 16 | client = TestOAuthClient(SERVER_URL, CONSUMER_KEY, CONSUMER_SECRET) 17 | client.start_fetch_request_token() 18 | 19 | print "After logging in and authorizing, input token key and secret..." 20 | 21 | request_token_key = raw_input("request token: ") 22 | request_token_secret = raw_input("request token secret: ") 23 | 24 | REQUEST_TOKEN = OAuthToken(request_token_key, request_token_secret) 25 | 26 | def get_access_token(): 27 | global ACCESS_TOKEN 28 | 29 | print "Fetching access token..." 30 | client = TestOAuthClient(SERVER_URL, CONSUMER_KEY, CONSUMER_SECRET) 31 | ACCESS_TOKEN = client.fetch_access_token(REQUEST_TOKEN) 32 | 33 | def get_api_resource(): 34 | 35 | resource_url = raw_input("Resource relative url (/api/v1/playlists): ") or "/api/v1/playlists" 36 | 37 | client = TestOAuthClient(SERVER_URL, CONSUMER_KEY, CONSUMER_SECRET) 38 | response = client.access_resource(resource_url, ACCESS_TOKEN) 39 | 40 | print "\n" 41 | print response 42 | print "\n" 43 | 44 | def run_tests(): 45 | global CONSUMER_KEY, CONSUMER_SECRET, SERVER_URL 46 | CONSUMER_KEY = raw_input("consumer key (anyone): ") or "anyone" 47 | CONSUMER_SECRET = raw_input("consumer secret (anyone): ") or "anyone" 48 | SERVER_URL = raw_input("server base url (http://local.kamenstestapp.appspot.com:8084): ") or "http://local.kamenstestapp.appspot.com:8084" 49 | 50 | get_request_token() 51 | if not REQUEST_TOKEN: 52 | print "Did not get request token." 53 | return 54 | 55 | get_access_token() 56 | if not ACCESS_TOKEN: 57 | print "Did not get access token." 58 | return 59 | 60 | while(True): 61 | try: 62 | get_api_resource() 63 | except Exception, e: 64 | print "Error: %s" % e 65 | 66 | def main(): 67 | run_tests() 68 | 69 | if __name__ == "__main__": 70 | main() 71 | -------------------------------------------------------------------------------- /examples/test_client/test_oauth_client.py: -------------------------------------------------------------------------------- 1 | import cgi 2 | import logging 3 | import urllib2 4 | import urlparse 5 | import webbrowser 6 | 7 | from oauth import OAuthConsumer, OAuthToken, OAuthRequest, OAuthSignatureMethod_HMAC_SHA1 8 | 9 | class TestOAuthClient(object): 10 | 11 | def __init__(self, server_url, consumer_key, consumer_secret): 12 | self.server_url = server_url 13 | self.consumer = OAuthConsumer(consumer_key, consumer_secret) 14 | 15 | def start_fetch_request_token(self): 16 | oauth_request = OAuthRequest.from_consumer_and_token( 17 | self.consumer, 18 | http_url = "%s/api/auth/request_token" % self.server_url 19 | ) 20 | 21 | oauth_request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer, None) 22 | webbrowser.open(oauth_request.to_url()) 23 | 24 | def fetch_access_token(self, request_token): 25 | 26 | oauth_request = OAuthRequest.from_consumer_and_token( 27 | self.consumer, 28 | token = request_token, 29 | http_url = "%s/api/auth/access_token" % self.server_url 30 | ) 31 | 32 | oauth_request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer, request_token) 33 | 34 | response = get_response(oauth_request.to_url()) 35 | 36 | return OAuthToken.from_string(response) 37 | 38 | def access_resource(self, relative_url, access_token, method="GET"): 39 | 40 | full_url = self.server_url + relative_url 41 | url = urlparse.urlparse(full_url) 42 | query_params = cgi.parse_qs(url.query) 43 | for key in query_params: 44 | query_params[key] = query_params[key][0] 45 | 46 | oauth_request = OAuthRequest.from_consumer_and_token( 47 | self.consumer, 48 | token = access_token, 49 | http_url = full_url, 50 | parameters = query_params, 51 | http_method=method 52 | ) 53 | 54 | oauth_request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer, access_token) 55 | 56 | if method == "GET": 57 | response = get_response(oauth_request.to_url()) 58 | else: 59 | response = post_response(full_url, oauth_request.to_postdata()) 60 | 61 | return response.strip() 62 | 63 | def get_response(url): 64 | response = "" 65 | file = None 66 | try: 67 | file = urllib2.urlopen(url) 68 | response = file.read() 69 | finally: 70 | if file: 71 | file.close() 72 | 73 | return response 74 | 75 | def post_response(url, data): 76 | response = "" 77 | file = None 78 | try: 79 | file = urllib2.urlopen(url, data) 80 | response = file.read() 81 | finally: 82 | if file: 83 | file.close() 84 | 85 | return response 86 | -------------------------------------------------------------------------------- /examples/test_client/oauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007 Leah Culver 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import logging 26 | logger = logging.getLogger() 27 | 28 | import cgi 29 | import urllib 30 | import time 31 | import random 32 | import urlparse 33 | import hmac 34 | import binascii 35 | 36 | 37 | VERSION = '1.0' # Hi Blaine! 38 | HTTP_METHOD = 'GET' 39 | SIGNATURE_METHOD = 'PLAINTEXT' 40 | 41 | 42 | class OAuthError(RuntimeError): 43 | """Generic exception class.""" 44 | def __init__(self, message='OAuth error occured.'): 45 | self.message = message 46 | 47 | def build_authenticate_header(realm=''): 48 | """Optional WWW-Authenticate header (401 error)""" 49 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 50 | 51 | def escape(s): 52 | """Escape a URL including any /.""" 53 | return urllib.quote(s, safe='~') 54 | 55 | def _utf8_str(s): 56 | """Convert unicode to utf-8.""" 57 | if isinstance(s, unicode): 58 | return s.encode("utf-8") 59 | else: 60 | return str(s) 61 | 62 | def generate_timestamp(): 63 | """Get seconds since epoch (UTC).""" 64 | return int(time.time()) 65 | 66 | def generate_nonce(length=8): 67 | """Generate pseudorandom number.""" 68 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 69 | 70 | def generate_verifier(length=8): 71 | """Generate pseudorandom number.""" 72 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 73 | 74 | 75 | class OAuthConsumer(object): 76 | """Consumer of OAuth authentication. 77 | 78 | OAuthConsumer is a data type that represents the identity of the Consumer 79 | via its shared secret with the Service Provider. 80 | 81 | """ 82 | key = None 83 | secret = None 84 | 85 | def __init__(self, key, secret): 86 | self.key = key 87 | self.secret = secret 88 | 89 | 90 | class OAuthToken(object): 91 | """OAuthToken is a data type that represents an End User via either an access 92 | or request token. 93 | 94 | key -- the token 95 | secret -- the token secret 96 | 97 | """ 98 | key = None 99 | secret = None 100 | callback = None 101 | callback_confirmed = None 102 | verifier = None 103 | 104 | def __init__(self, key, secret): 105 | self.key = key 106 | self.secret = secret 107 | 108 | def set_callback(self, callback): 109 | self.callback = callback 110 | self.callback_confirmed = 'true' 111 | 112 | def set_verifier(self, verifier=None): 113 | if verifier is not None: 114 | self.verifier = verifier 115 | else: 116 | self.verifier = generate_verifier() 117 | 118 | def get_callback_url(self): 119 | if self.callback and self.verifier: 120 | # Append the oauth_verifier. 121 | parts = urlparse.urlparse(self.callback) 122 | scheme, netloc, path, params, query, fragment = parts[:6] 123 | if query: 124 | query = '%s&oauth_verifier=%s' % (query, self.verifier) 125 | else: 126 | query = 'oauth_verifier=%s' % self.verifier 127 | return urlparse.urlunparse((scheme, netloc, path, params, 128 | query, fragment)) 129 | return self.callback 130 | 131 | def to_string(self): 132 | data = { 133 | 'oauth_token': self.key, 134 | 'oauth_token_secret': self.secret, 135 | } 136 | if self.callback_confirmed is not None: 137 | data['oauth_callback_confirmed'] = self.callback_confirmed 138 | return urllib.urlencode(data) 139 | 140 | def from_string(s): 141 | """ Returns a token from something like: 142 | oauth_token_secret=xxx&oauth_token=xxx 143 | """ 144 | params = cgi.parse_qs(s, keep_blank_values=False) 145 | key = params['oauth_token'][0] 146 | secret = params['oauth_token_secret'][0] 147 | token = OAuthToken(key, secret) 148 | try: 149 | token.callback_confirmed = params['oauth_callback_confirmed'][0] 150 | except KeyError: 151 | pass # 1.0, no callback confirmed. 152 | return token 153 | from_string = staticmethod(from_string) 154 | 155 | def __str__(self): 156 | return self.to_string() 157 | 158 | 159 | class OAuthRequest(object): 160 | """OAuthRequest represents the request and can be serialized. 161 | 162 | OAuth parameters: 163 | - oauth_consumer_key 164 | - oauth_token 165 | - oauth_signature_method 166 | - oauth_signature 167 | - oauth_timestamp 168 | - oauth_nonce 169 | - oauth_version 170 | - oauth_verifier 171 | ... any additional parameters, as defined by the Service Provider. 172 | """ 173 | parameters = None # OAuth parameters. 174 | http_method = HTTP_METHOD 175 | http_url = None 176 | version = VERSION 177 | 178 | def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): 179 | self.http_method = http_method 180 | self.http_url = http_url 181 | self.parameters = parameters or {} 182 | 183 | def set_parameter(self, parameter, value): 184 | self.parameters[parameter] = value 185 | 186 | def get_parameter(self, parameter): 187 | try: 188 | return self.parameters[parameter] 189 | except: 190 | raise OAuthError('Parameter not found: %s' % parameter) 191 | 192 | def _get_timestamp_nonce(self): 193 | return self.get_parameter('oauth_timestamp'), self.get_parameter( 194 | 'oauth_nonce') 195 | 196 | def get_nonoauth_parameters(self): 197 | """Get any non-OAuth parameters.""" 198 | parameters = {} 199 | for k, v in self.parameters.iteritems(): 200 | # Ignore oauth parameters. 201 | if k.find('oauth_') < 0: 202 | parameters[k] = v 203 | return parameters 204 | 205 | def to_header(self, realm=''): 206 | """Serialize as a header for an HTTPAuth request.""" 207 | auth_header = 'OAuth realm="%s"' % realm 208 | # Add the oauth parameters. 209 | if self.parameters: 210 | for k, v in self.parameters.iteritems(): 211 | if k[:6] == 'oauth_': 212 | auth_header += ', %s="%s"' % (k, escape(str(v))) 213 | return {'Authorization': auth_header} 214 | 215 | def to_postdata(self): 216 | """Serialize as post data for a POST request.""" 217 | return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \ 218 | for k, v in self.parameters.iteritems()]) 219 | 220 | def to_url(self): 221 | """Serialize as a URL for a GET request.""" 222 | return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) 223 | 224 | def get_normalized_parameters(self): 225 | """Return a string that contains the parameters that must be signed.""" 226 | params = self.parameters 227 | try: 228 | # Exclude the signature if it exists. 229 | del params['oauth_signature'] 230 | except: 231 | pass 232 | # Escape key values before sorting. 233 | key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \ 234 | for k,v in params.items()] 235 | # Sort lexicographically, first after key, then after value. 236 | key_values.sort() 237 | # Combine key value pairs into a string. 238 | return '&'.join(['%s=%s' % (k, v) for k, v in key_values]) 239 | 240 | def get_normalized_http_method(self): 241 | """Uppercases the http method.""" 242 | return self.http_method.upper() 243 | 244 | def get_normalized_http_url(self): 245 | """Parses the URL and rebuilds it to be scheme://host/path.""" 246 | parts = urlparse.urlparse(self.http_url) 247 | scheme, netloc, path = parts[:3] 248 | # Exclude default port numbers. 249 | if scheme == 'http' and netloc[-3:] == ':80': 250 | netloc = netloc[:-3] 251 | elif scheme == 'https' and netloc[-4:] == ':443': 252 | netloc = netloc[:-4] 253 | return '%s://%s%s' % (scheme, netloc, path) 254 | 255 | def sign_request(self, signature_method, consumer, token): 256 | """Set the signature parameter to the result of build_signature.""" 257 | # Set the signature method. 258 | self.set_parameter('oauth_signature_method', 259 | signature_method.get_name()) 260 | # Set the signature. 261 | self.set_parameter('oauth_signature', 262 | self.build_signature(signature_method, consumer, token)) 263 | 264 | def build_signature(self, signature_method, consumer, token): 265 | """Calls the build signature method within the signature method.""" 266 | return signature_method.build_signature(self, consumer, token) 267 | 268 | def from_request(http_method, http_url, headers=None, parameters=None, 269 | query_string=None): 270 | """Combines multiple parameter sources.""" 271 | if parameters is None: 272 | parameters = {} 273 | 274 | # Headers 275 | if headers and 'Authorization' in headers: 276 | auth_header = headers['Authorization'] 277 | # Check that the authorization header is OAuth. 278 | if auth_header[:6] == 'OAuth ': 279 | auth_header = auth_header[6:] 280 | try: 281 | # Get the parameters from the header. 282 | header_params = OAuthRequest._split_header(auth_header) 283 | parameters.update(header_params) 284 | except: 285 | raise OAuthError('Unable to parse OAuth parameters from ' 286 | 'Authorization header.') 287 | 288 | # GET or POST query string. 289 | if query_string: 290 | query_params = OAuthRequest._split_url_string(query_string) 291 | parameters.update(query_params) 292 | 293 | # URL parameters. 294 | param_str = urlparse.urlparse(http_url)[4] # query 295 | url_params = OAuthRequest._split_url_string(param_str) 296 | parameters.update(url_params) 297 | 298 | if parameters: 299 | return OAuthRequest(http_method, http_url, parameters) 300 | 301 | return None 302 | from_request = staticmethod(from_request) 303 | 304 | def from_consumer_and_token(oauth_consumer, token=None, 305 | callback=None, verifier=None, http_method=HTTP_METHOD, 306 | http_url=None, parameters=None): 307 | if not parameters: 308 | parameters = {} 309 | 310 | defaults = { 311 | 'oauth_consumer_key': oauth_consumer.key, 312 | 'oauth_timestamp': generate_timestamp(), 313 | 'oauth_nonce': generate_nonce(), 314 | 'oauth_version': OAuthRequest.version, 315 | } 316 | 317 | defaults.update(parameters) 318 | parameters = defaults 319 | 320 | if token: 321 | parameters['oauth_token'] = token.key 322 | if token.callback: 323 | parameters['oauth_callback'] = token.callback 324 | # 1.0a support for verifier. 325 | if verifier: 326 | parameters['oauth_verifier'] = verifier 327 | elif callback: 328 | # 1.0a support for callback in the request token request. 329 | parameters['oauth_callback'] = callback 330 | 331 | return OAuthRequest(http_method, http_url, parameters) 332 | from_consumer_and_token = staticmethod(from_consumer_and_token) 333 | 334 | def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, 335 | http_url=None, parameters=None): 336 | if not parameters: 337 | parameters = {} 338 | 339 | parameters['oauth_token'] = token.key 340 | 341 | if callback: 342 | parameters['oauth_callback'] = callback 343 | 344 | return OAuthRequest(http_method, http_url, parameters) 345 | from_token_and_callback = staticmethod(from_token_and_callback) 346 | 347 | def _split_header(header): 348 | """Turn Authorization: header into parameters.""" 349 | params = {} 350 | parts = header.split(',') 351 | for param in parts: 352 | # Ignore realm parameter. 353 | if param.find('realm') > -1: 354 | continue 355 | # Remove whitespace. 356 | param = param.strip() 357 | # Split key-value. 358 | param_parts = param.split('=', 1) 359 | # Remove quotes and unescape the value. 360 | params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) 361 | return params 362 | _split_header = staticmethod(_split_header) 363 | 364 | def _split_url_string(param_str): 365 | """Turn URL string into parameters.""" 366 | parameters = cgi.parse_qs(param_str, keep_blank_values=False) 367 | for k, v in parameters.iteritems(): 368 | parameters[k] = urllib.unquote(v[0]) 369 | return parameters 370 | _split_url_string = staticmethod(_split_url_string) 371 | 372 | class OAuthServer(object): 373 | """A worker to check the validity of a request against a data store.""" 374 | timestamp_threshold = 300 # In seconds, five minutes. 375 | version = VERSION 376 | signature_methods = None 377 | data_store = None 378 | 379 | def __init__(self, data_store=None, signature_methods=None): 380 | self.data_store = data_store 381 | self.signature_methods = signature_methods or {} 382 | 383 | def set_data_store(self, data_store): 384 | self.data_store = data_store 385 | 386 | def get_data_store(self): 387 | return self.data_store 388 | 389 | def add_signature_method(self, signature_method): 390 | self.signature_methods[signature_method.get_name()] = signature_method 391 | return self.signature_methods 392 | 393 | def fetch_request_token(self, oauth_request): 394 | """Processes a request_token request and returns the 395 | request token on success. 396 | """ 397 | try: 398 | # Get the request token for authorization. 399 | token = self._get_token(oauth_request, 'request') 400 | except OAuthError: 401 | # No token required for the initial token request. 402 | version = self._get_version(oauth_request) 403 | consumer = self._get_consumer(oauth_request) 404 | try: 405 | callback = self.get_callback(oauth_request) 406 | except OAuthError: 407 | callback = None # 1.0, no callback specified. 408 | self._check_signature(oauth_request, consumer, None) 409 | # Fetch a new token. 410 | token = self.data_store.fetch_request_token(consumer, callback) 411 | return token 412 | 413 | def fetch_access_token(self, oauth_request): 414 | logger.debug("!!! IN OAuthServer.fetch_access_token OAuth Params: %s"%oauth_request.parameters) 415 | 416 | """Processes an access_token request and returns the 417 | access token on success. 418 | """ 419 | version = self._get_version(oauth_request) 420 | consumer = self._get_consumer(oauth_request) 421 | try: 422 | verifier = self._get_verifier(oauth_request) 423 | except OAuthError: 424 | verifier = None 425 | # Get the request token. 426 | token = self._get_token(oauth_request, 'request') 427 | self._check_signature(oauth_request, consumer, token) 428 | new_token = self.data_store.fetch_access_token(consumer, token, verifier) 429 | return new_token 430 | 431 | def verify_request(self, oauth_request): 432 | """Verifies an api call and checks all the parameters.""" 433 | # -> consumer and token 434 | version = self._get_version(oauth_request) 435 | consumer = self._get_consumer(oauth_request) 436 | # Get the access token. 437 | token = self._get_token(oauth_request, 'access') 438 | self._check_signature(oauth_request, consumer, token) 439 | parameters = oauth_request.get_nonoauth_parameters() 440 | return consumer, token, parameters 441 | 442 | def authorize_token(self, token, user): 443 | """Authorize a request token.""" 444 | return self.data_store.authorize_request_token(token, user) 445 | 446 | def get_callback(self, oauth_request): 447 | """Get the callback URL.""" 448 | return oauth_request.get_parameter('oauth_callback') 449 | 450 | def build_authenticate_header(self, realm=''): 451 | """Optional support for the authenticate header.""" 452 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 453 | 454 | def _get_version(self, oauth_request): 455 | """Verify the correct version request for this server.""" 456 | try: 457 | version = oauth_request.get_parameter('oauth_version') 458 | except: 459 | version = VERSION 460 | if version and version != self.version: 461 | raise OAuthError('OAuth version %s not supported.' % str(version)) 462 | return version 463 | 464 | def _get_signature_method(self, oauth_request): 465 | """Figure out the signature with some defaults.""" 466 | try: 467 | signature_method = oauth_request.get_parameter( 468 | 'oauth_signature_method') 469 | except: 470 | signature_method = SIGNATURE_METHOD 471 | try: 472 | # Get the signature method object. 473 | signature_method = self.signature_methods[signature_method] 474 | except: 475 | signature_method_names = ', '.join(self.signature_methods.keys()) 476 | raise OAuthError('Signature method %s not supported try one of the ' 477 | 'following: %s' % (signature_method, signature_method_names)) 478 | 479 | return signature_method 480 | 481 | def _get_consumer(self, oauth_request): 482 | consumer_key = oauth_request.get_parameter('oauth_consumer_key') 483 | consumer = self.data_store.lookup_consumer(consumer_key) 484 | if not consumer: 485 | raise OAuthError('Invalid consumer.') 486 | return consumer 487 | 488 | def _get_token(self, oauth_request, token_type='access'): 489 | """Try to find the token for the provided request token key.""" 490 | token_field = oauth_request.get_parameter('oauth_token') 491 | token = self.data_store.lookup_token(token_type, token_field) 492 | if not token: 493 | raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) 494 | return token 495 | 496 | def _get_verifier(self, oauth_request): 497 | return oauth_request.get_parameter('oauth_verifier') 498 | 499 | def _check_signature(self, oauth_request, consumer, token): 500 | timestamp, nonce = oauth_request._get_timestamp_nonce() 501 | self._check_timestamp(timestamp) 502 | self._check_nonce(consumer, token, nonce) 503 | signature_method = self._get_signature_method(oauth_request) 504 | try: 505 | signature = oauth_request.get_parameter('oauth_signature') 506 | except: 507 | raise OAuthError('Missing signature.') 508 | # Validate the signature. 509 | valid_sig = signature_method.check_signature(oauth_request, consumer, 510 | token, signature) 511 | if not valid_sig: 512 | key, base = signature_method.build_signature_base_string( 513 | oauth_request, consumer, token) 514 | logging.error("key: %s",key) 515 | logging.error("base: %s",base) 516 | raise OAuthError('Invalid signature. Expected signature base ' 517 | 'string: %s' % base) 518 | built = signature_method.build_signature(oauth_request, consumer, token) 519 | 520 | def _check_timestamp(self, timestamp): 521 | """Verify that timestamp is recentish.""" 522 | timestamp = int(timestamp) 523 | now = int(time.time()) 524 | lapsed = abs(now - timestamp) 525 | if lapsed > self.timestamp_threshold: 526 | raise OAuthError('Expired timestamp: given %d and now %s has a ' 527 | 'greater difference than threshold %d' % 528 | (timestamp, now, self.timestamp_threshold)) 529 | 530 | def _check_nonce(self, consumer, token, nonce): 531 | """Verify that the nonce is uniqueish.""" 532 | nonce = self.data_store.lookup_nonce(consumer, token, nonce) 533 | if nonce: 534 | raise OAuthError('Nonce already used: %s' % str(nonce)) 535 | 536 | 537 | class OAuthClient(object): 538 | """OAuthClient is a worker to attempt to execute a request.""" 539 | consumer = None 540 | token = None 541 | 542 | def __init__(self, oauth_consumer, oauth_token): 543 | self.consumer = oauth_consumer 544 | self.token = oauth_token 545 | 546 | def get_consumer(self): 547 | return self.consumer 548 | 549 | def get_token(self): 550 | return self.token 551 | 552 | def fetch_request_token(self, oauth_request): 553 | """-> OAuthToken.""" 554 | raise NotImplementedError 555 | 556 | def fetch_access_token(self, oauth_request): 557 | """-> OAuthToken.""" 558 | raise NotImplementedError 559 | 560 | def access_resource(self, oauth_request): 561 | """-> Some protected resource.""" 562 | raise NotImplementedError 563 | 564 | 565 | class OAuthDataStore(object): 566 | """A database abstraction used to lookup consumers and tokens.""" 567 | 568 | def lookup_consumer(self, key): 569 | """-> OAuthConsumer.""" 570 | raise NotImplementedError 571 | 572 | def lookup_token(self, oauth_consumer, token_type, token_token): 573 | """-> OAuthToken.""" 574 | raise NotImplementedError 575 | 576 | def lookup_nonce(self, oauth_consumer, oauth_token, nonce): 577 | """-> OAuthToken.""" 578 | raise NotImplementedError 579 | 580 | def fetch_request_token(self, oauth_consumer, oauth_callback): 581 | """-> OAuthToken.""" 582 | raise NotImplementedError 583 | 584 | def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier): 585 | """-> OAuthToken.""" 586 | raise NotImplementedError 587 | 588 | def authorize_request_token(self, oauth_token, user): 589 | """-> OAuthToken.""" 590 | raise NotImplementedError 591 | 592 | 593 | class OAuthSignatureMethod(object): 594 | """A strategy class that implements a signature method.""" 595 | def get_name(self): 596 | """-> str.""" 597 | raise NotImplementedError 598 | 599 | def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): 600 | """-> str key, str raw.""" 601 | raise NotImplementedError 602 | 603 | def build_signature(self, oauth_request, oauth_consumer, oauth_token): 604 | """-> str.""" 605 | raise NotImplementedError 606 | 607 | def check_signature(self, oauth_request, consumer, token, signature): 608 | built = self.build_signature(oauth_request, consumer, token) 609 | logging.info("Built signature: %s"%(built)) 610 | return built == signature 611 | 612 | 613 | class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): 614 | 615 | def get_name(self): 616 | return 'HMAC-SHA1' 617 | 618 | def build_signature_base_string(self, oauth_request, consumer, token): 619 | sig = ( 620 | escape(oauth_request.get_normalized_http_method()), 621 | escape(oauth_request.get_normalized_http_url()), 622 | escape(oauth_request.get_normalized_parameters()), 623 | ) 624 | 625 | key = '%s&' % escape(consumer.secret) 626 | if token: 627 | key += escape(token.secret) 628 | raw = '&'.join(sig) 629 | return key, raw 630 | 631 | def build_signature(self, oauth_request, consumer, token): 632 | """Builds the base signature string.""" 633 | key, raw = self.build_signature_base_string(oauth_request, consumer, 634 | token) 635 | 636 | if isinstance(key, unicode): 637 | key = str(key) 638 | 639 | # HMAC object. 640 | try: 641 | import hashlib # 2.5 642 | hashed = hmac.new(key, raw, hashlib.sha1) 643 | except: 644 | import sha # Deprecated 645 | hashed = hmac.new(key, raw, sha) 646 | 647 | # Calculate the digest base 64. 648 | return binascii.b2a_base64(hashed.digest())[:-1] 649 | 650 | 651 | class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): 652 | 653 | def get_name(self): 654 | return 'PLAINTEXT' 655 | 656 | def build_signature_base_string(self, oauth_request, consumer, token): 657 | """Concatenates the consumer key and secret.""" 658 | sig = '%s&' % escape(consumer.secret) 659 | if token: 660 | sig = sig + escape(token.secret) 661 | return sig, sig 662 | 663 | def build_signature(self, oauth_request, consumer, token): 664 | key, raw = self.build_signature_base_string(oauth_request, consumer, 665 | token) 666 | return key 667 | --------------------------------------------------------------------------------