├── .gitignore ├── oauth.py └── sample.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .project 4 | .pydevproject 5 | .settings 6 | 7 | -------------------------------------------------------------------------------- /oauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | A simple OAuth implementation for authenticating users with third party 5 | websites. 6 | 7 | A typical use case inside an AppEngine controller would be: 8 | 9 | 1) Create the OAuth client. In this case we'll use the Twitter client, 10 | but you could write other clients to connect to different services. 11 | 12 | import oauth 13 | 14 | consumer_key = "LKlkj83kaio2fjiudjd9...etc" 15 | consumer_secret = "58kdujslkfojkjsjsdk...etc" 16 | callback_url = "http://www.myurl.com/callback/twitter" 17 | 18 | client = oauth.TwitterClient(consumer_key, consumer_secret, callback_url) 19 | 20 | 2) Send the user to Twitter in order to login: 21 | 22 | self.redirect(client.get_authorization_url()) 23 | 24 | 3) Once the user has arrived back at your callback URL, you'll want to 25 | get the authenticated user information. 26 | 27 | auth_token = self.request.get("oauth_token") 28 | auth_verifier = self.request.get("oauth_verifier") 29 | user_info = client.get_user_info(auth_token, auth_verifier=auth_verifier) 30 | 31 | The "user_info" variable should then contain a dictionary of various 32 | user information (id, picture url, etc). What you do with that data is up 33 | to you. 34 | 35 | That's it! 36 | 37 | 4) If you need to, you can also call other other API URLs using 38 | client.make_request() as long as you supply a valid API URL and an access 39 | token and secret. Note, you may need to set method=urlfetch.POST. 40 | 41 | @author: Mike Knapp 42 | @copyright: Unrestricted. Feel free to use modify however you see fit. Please 43 | note however this software is unsupported. Please don't email me about it. :) 44 | """ 45 | 46 | from google.appengine.api import memcache 47 | from google.appengine.api import urlfetch 48 | from google.appengine.ext import db 49 | 50 | from cgi import parse_qs 51 | from django.utils import simplejson as json 52 | from hashlib import sha1 53 | from hmac import new as hmac 54 | from random import getrandbits 55 | from time import time 56 | from urllib import urlencode 57 | from urllib import quote as urlquote 58 | from urllib import unquote as urlunquote 59 | 60 | import logging 61 | 62 | 63 | class OAuthException(Exception): 64 | pass 65 | 66 | 67 | def get_oauth_client(service, key, secret, callback_url): 68 | """Get OAuth Client. 69 | 70 | A factory that will return the appropriate OAuth client. 71 | """ 72 | 73 | if service == "twitter": 74 | return TwitterClient(key, secret, callback_url) 75 | elif service == "yahoo": 76 | return YahooClient(key, secret, callback_url) 77 | elif service == "myspace": 78 | return MySpaceClient(key, secret, callback_url) 79 | else: 80 | raise Exception, "Unknown OAuth service %s" % service 81 | 82 | 83 | class AuthToken(db.Model): 84 | """Auth Token. 85 | 86 | A temporary auth token that we will use to authenticate a user with a 87 | third party website. (We need to store the data while the user visits 88 | the third party website to authenticate themselves.) 89 | 90 | TODO: Implement a cron to clean out old tokens periodically. 91 | """ 92 | 93 | service = db.StringProperty(required=True) 94 | token = db.StringProperty(required=True) 95 | secret = db.StringProperty(required=True) 96 | created = db.DateTimeProperty(auto_now_add=True) 97 | 98 | 99 | class OAuthClient(): 100 | 101 | def __init__(self, service_name, consumer_key, consumer_secret, request_url, 102 | access_url, callback_url=None): 103 | """ Constructor.""" 104 | 105 | self.service_name = service_name 106 | self.consumer_key = consumer_key 107 | self.consumer_secret = consumer_secret 108 | self.request_url = request_url 109 | self.access_url = access_url 110 | self.callback_url = callback_url 111 | 112 | def prepare_request(self, url, token="", secret="", additional_params=None, 113 | method=urlfetch.GET): 114 | """Prepare Request. 115 | 116 | Prepares an authenticated request to any OAuth protected resource. 117 | 118 | Returns the payload of the request. 119 | """ 120 | 121 | def encode(text): 122 | return urlquote(str(text), "") 123 | 124 | params = { 125 | "oauth_consumer_key": self.consumer_key, 126 | "oauth_signature_method": "HMAC-SHA1", 127 | "oauth_timestamp": str(int(time())), 128 | "oauth_nonce": str(getrandbits(64)), 129 | "oauth_version": "1.0" 130 | } 131 | 132 | if token: 133 | params["oauth_token"] = token 134 | elif self.callback_url: 135 | params["oauth_callback"] = self.callback_url 136 | 137 | if additional_params: 138 | params.update(additional_params) 139 | 140 | for k,v in params.items(): 141 | if isinstance(v, unicode): 142 | params[k] = v.encode('utf8') 143 | 144 | # Join all of the params together. 145 | params_str = "&".join(["%s=%s" % (encode(k), encode(params[k])) 146 | for k in sorted(params)]) 147 | 148 | # Join the entire message together per the OAuth specification. 149 | message = "&".join(["GET" if method == urlfetch.GET else "POST", 150 | encode(url), encode(params_str)]) 151 | 152 | # Create a HMAC-SHA1 signature of the message. 153 | key = "%s&%s" % (self.consumer_secret, secret) # Note compulsory "&". 154 | signature = hmac(key, message, sha1) 155 | digest_base64 = signature.digest().encode("base64").strip() 156 | params["oauth_signature"] = digest_base64 157 | 158 | # Construct the request payload and return it 159 | return urlencode(params) 160 | 161 | 162 | def make_async_request(self, url, token="", secret="", additional_params=None, 163 | protected=False, method=urlfetch.GET): 164 | """Make Request. 165 | 166 | Make an authenticated request to any OAuth protected resource. 167 | 168 | If protected is equal to True, the Authorization: OAuth header will be set. 169 | 170 | A urlfetch response object is returned. 171 | """ 172 | payload = self.prepare_request(url, token, secret, additional_params, 173 | method) 174 | if method == urlfetch.GET: 175 | url = "%s?%s" % (url, payload) 176 | payload = None 177 | headers = {"Authorization": "OAuth"} if protected else {} 178 | rpc = urlfetch.create_rpc(deadline=10.0) 179 | urlfetch.make_fetch_call(rpc, url, method=method, headers=headers, payload=payload) 180 | return rpc 181 | 182 | def make_request(self, url, token="", secret="", additional_params=None, 183 | protected=False, method=urlfetch.GET): 184 | return self.make_async_request(url, token, secret, additional_params, protected, method).get_result() 185 | 186 | def get_authorization_url(self): 187 | """Get Authorization URL. 188 | 189 | Returns a service specific URL which contains an auth token. The user 190 | should be redirected to this URL so that they can give consent to be 191 | logged in. 192 | """ 193 | 194 | raise NotImplementedError, "Must be implemented by a subclass" 195 | 196 | def get_user_info(self, auth_token, auth_verifier=""): 197 | """Get User Info. 198 | 199 | Exchanges the auth token for an access token and returns a dictionary 200 | of information about the authenticated user. 201 | """ 202 | 203 | auth_token = urlunquote(auth_token) 204 | auth_verifier = urlunquote(auth_verifier) 205 | 206 | auth_secret = memcache.get(self._get_memcache_auth_key(auth_token)) 207 | 208 | if not auth_secret: 209 | result = AuthToken.gql(""" 210 | WHERE 211 | service = :1 AND 212 | token = :2 213 | LIMIT 214 | 1 215 | """, self.service_name, auth_token).get() 216 | 217 | if not result: 218 | logging.error("The auth token %s was not found in our db" % auth_token) 219 | raise Exception, "Could not find Auth Token in database" 220 | else: 221 | auth_secret = result.secret 222 | 223 | response = self.make_request(self.access_url, 224 | token=auth_token, 225 | secret=auth_secret, 226 | additional_params={"oauth_verifier": 227 | auth_verifier}) 228 | 229 | # Extract the access token/secret from the response. 230 | result = self._extract_credentials(response) 231 | 232 | # Try to collect some information about this user from the service. 233 | user_info = self._lookup_user_info(result["token"], result["secret"]) 234 | user_info.update(result) 235 | 236 | return user_info 237 | 238 | def _get_auth_token(self): 239 | """Get Authorization Token. 240 | 241 | Actually gets the authorization token and secret from the service. The 242 | token and secret are stored in our database, and the auth token is 243 | returned. 244 | """ 245 | 246 | response = self.make_request(self.request_url) 247 | result = self._extract_credentials(response) 248 | 249 | auth_token = result["token"] 250 | auth_secret = result["secret"] 251 | 252 | # Save the auth token and secret in our database. 253 | auth = AuthToken(service=self.service_name, 254 | token=auth_token, 255 | secret=auth_secret) 256 | auth.put() 257 | 258 | # Add the secret to memcache as well. 259 | memcache.set(self._get_memcache_auth_key(auth_token), auth_secret, 260 | time=20*60) 261 | 262 | return auth_token 263 | 264 | def _get_memcache_auth_key(self, auth_token): 265 | 266 | return "oauth_%s_%s" % (self.service_name, auth_token) 267 | 268 | def _extract_credentials(self, result): 269 | """Extract Credentials. 270 | 271 | Returns an dictionary containing the token and secret (if present). 272 | Throws an Exception otherwise. 273 | """ 274 | 275 | token = None 276 | secret = None 277 | parsed_results = parse_qs(result.content) 278 | 279 | if "oauth_token" in parsed_results: 280 | token = parsed_results["oauth_token"][0] 281 | 282 | if "oauth_token_secret" in parsed_results: 283 | secret = parsed_results["oauth_token_secret"][0] 284 | 285 | if not (token and secret) or result.status_code != 200: 286 | logging.error("Could not extract token/secret: %s" % result.content) 287 | raise OAuthException("Problem talking to the service") 288 | 289 | return { 290 | "service": self.service_name, 291 | "token": token, 292 | "secret": secret 293 | } 294 | 295 | def _lookup_user_info(self, access_token, access_secret): 296 | """Lookup User Info. 297 | 298 | Complies a dictionary describing the user. The user should be 299 | authenticated at this point. Each different client should override 300 | this method. 301 | """ 302 | 303 | raise NotImplementedError, "Must be implemented by a subclass" 304 | 305 | def _get_default_user_info(self): 306 | """Get Default User Info. 307 | 308 | Returns a blank array that can be used to populate generalized user 309 | information. 310 | """ 311 | 312 | return { 313 | "id": "", 314 | "username": "", 315 | "name": "", 316 | "picture": "" 317 | } 318 | 319 | 320 | class TwitterClient(OAuthClient): 321 | """Twitter Client. 322 | 323 | A client for talking to the Twitter API using OAuth as the 324 | authentication model. 325 | """ 326 | 327 | def __init__(self, consumer_key, consumer_secret, callback_url): 328 | """Constructor.""" 329 | 330 | OAuthClient.__init__(self, 331 | "twitter", 332 | consumer_key, 333 | consumer_secret, 334 | "http://twitter.com/oauth/request_token", 335 | "http://twitter.com/oauth/access_token", 336 | callback_url) 337 | 338 | def get_authorization_url(self): 339 | """Get Authorization URL.""" 340 | 341 | token = self._get_auth_token() 342 | return "http://twitter.com/oauth/authorize?oauth_token=%s" % token 343 | 344 | def _lookup_user_info(self, access_token, access_secret): 345 | """Lookup User Info. 346 | 347 | Lookup the user on Twitter. 348 | """ 349 | 350 | response = self.make_request( 351 | "http://twitter.com/account/verify_credentials.json", 352 | token=access_token, secret=access_secret, protected=True) 353 | 354 | data = json.loads(response.content) 355 | 356 | user_info = self._get_default_user_info() 357 | user_info["id"] = data["id"] 358 | user_info["username"] = data["screen_name"] 359 | user_info["name"] = data["name"] 360 | user_info["picture"] = data["profile_image_url"] 361 | 362 | return user_info 363 | 364 | 365 | class MySpaceClient(OAuthClient): 366 | """MySpace Client. 367 | 368 | A client for talking to the MySpace API using OAuth as the 369 | authentication model. 370 | """ 371 | 372 | def __init__(self, consumer_key, consumer_secret, callback_url): 373 | """Constructor.""" 374 | 375 | OAuthClient.__init__(self, 376 | "myspace", 377 | consumer_key, 378 | consumer_secret, 379 | "http://api.myspace.com/request_token", 380 | "http://api.myspace.com/access_token", 381 | callback_url) 382 | 383 | def get_authorization_url(self): 384 | """Get Authorization URL.""" 385 | 386 | token = self._get_auth_token() 387 | return ("http://api.myspace.com/authorize?oauth_token=%s" 388 | "&oauth_callback=%s" % (token, urlquote(self.callback_url))) 389 | 390 | def _lookup_user_info(self, access_token, access_secret): 391 | """Lookup User Info. 392 | 393 | Lookup the user on MySpace. 394 | """ 395 | 396 | response = self.make_request("http://api.myspace.com/v1/user.json", 397 | token=access_token, secret=access_secret, protected=True) 398 | 399 | data = json.loads(response.content) 400 | 401 | user_info = self._get_default_user_info() 402 | user_info["id"] = data["userId"] 403 | username = data["webUri"].replace("http://www.myspace.com/", "") 404 | user_info["username"] = username 405 | user_info["name"] = data["name"] 406 | user_info["picture"] = data["image"] 407 | 408 | return user_info 409 | 410 | 411 | class YahooClient(OAuthClient): 412 | """Yahoo! Client. 413 | 414 | A client for talking to the Yahoo! API using OAuth as the 415 | authentication model. 416 | """ 417 | 418 | def __init__(self, consumer_key, consumer_secret, callback_url): 419 | """Constructor.""" 420 | 421 | OAuthClient.__init__(self, 422 | "yahoo", 423 | consumer_key, 424 | consumer_secret, 425 | "https://api.login.yahoo.com/oauth/v2/get_request_token", 426 | "https://api.login.yahoo.com/oauth/v2/get_token", 427 | callback_url) 428 | 429 | def get_authorization_url(self): 430 | """Get Authorization URL.""" 431 | 432 | token = self._get_auth_token() 433 | return ("https://api.login.yahoo.com/oauth/v2/request_auth?oauth_token=%s" 434 | % token) 435 | 436 | def _lookup_user_info(self, access_token, access_secret): 437 | """Lookup User Info. 438 | 439 | Lookup the user on Yahoo! 440 | """ 441 | 442 | user_info = self._get_default_user_info() 443 | 444 | # 1) Obtain the user's GUID. 445 | response = self.make_request( 446 | "http://social.yahooapis.com/v1/me/guid", token=access_token, 447 | secret=access_secret, additional_params={"format": "json"}, 448 | protected=True) 449 | 450 | data = json.loads(response.content)["guid"] 451 | guid = data["value"] 452 | 453 | # 2) Inspect the user's profile. 454 | response = self.make_request( 455 | "http://social.yahooapis.com/v1/user/%s/profile/usercard" % guid, 456 | token=access_token, secret=access_secret, 457 | additional_params={"format": "json"}, protected=True) 458 | 459 | data = json.loads(response.content)["profile"] 460 | 461 | user_info["id"] = guid 462 | user_info["username"] = data["nickname"].lower() 463 | user_info["name"] = data["nickname"] 464 | user_info["picture"] = data["image"]["imageUrl"] 465 | 466 | return user_info 467 | -------------------------------------------------------------------------------- /sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # This is an sample AppEngine application that shows how to 1) log in a user 4 | # using the Twitter OAuth API and 2) extract their timeline. 5 | # 6 | # INSTRUCTIONS: 7 | # 8 | # 1. Set up a new AppEngine application using this file, let's say on port 9 | # 8080. Rename this file to main.py, or alternatively modify your app.yaml 10 | # file.) 11 | # 2. Fill in the application ("consumer") key and secret lines below. 12 | # 3. Visit http://localhost:8080 and click the "login" link to be redirected 13 | # to Twitter.com. 14 | # 4. Once verified, you'll be redirected back to your app on localhost and 15 | # you'll see some of your Twitter user info printed in the browser. 16 | # 5. Copy and paste the token and secret info into this file, replacing the 17 | # default values for user_token and user_secret. You'll need the user's token 18 | # & secret info to interact with the Twitter API on their behalf from now on. 19 | # 6. Finally, visit http://localhost:8080/timeline to see your twitter 20 | # timeline. 21 | # 22 | 23 | __author__ = "Mike Knapp" 24 | 25 | import oauth 26 | 27 | from google.appengine.ext import webapp 28 | from google.appengine.ext.webapp import util 29 | 30 | 31 | class MainHandler(webapp.RequestHandler): 32 | 33 | def get(self, mode=""): 34 | 35 | # Your application Twitter application ("consumer") key and secret. 36 | # You'll need to register an application on Twitter first to get this 37 | # information: http://www.twitter.com/oauth 38 | application_key = "FILL_IN" 39 | application_secret = "FILL_IN" 40 | 41 | # Fill in the next 2 lines after you have successfully logged in to 42 | # Twitter per the instructions above. This is the *user's* token and 43 | # secret. You need these values to call the API on their behalf after 44 | # they have logged in to your app. 45 | user_token = "FILL_IN" 46 | user_secret = "FILL_IN" 47 | 48 | # In the real world, you'd want to edit this callback URL to point to your 49 | # production server. This is where the user is sent to after they have 50 | # authenticated with Twitter. 51 | callback_url = "%s/verify" % self.request.host_url 52 | 53 | client = oauth.TwitterClient(application_key, application_secret, 54 | callback_url) 55 | 56 | if mode == "login": 57 | return self.redirect(client.get_authorization_url()) 58 | 59 | if mode == "verify": 60 | auth_token = self.request.get("oauth_token") 61 | auth_verifier = self.request.get("oauth_verifier") 62 | user_info = client.get_user_info(auth_token, auth_verifier=auth_verifier) 63 | return self.response.out.write(user_info) 64 | 65 | if mode == "timeline": 66 | timeline_url = "http://twitter.com/statuses/user_timeline.xml" 67 | result = client.make_request(url=timeline_url, token=user_token, 68 | secret=user_secret) 69 | return self.response.out.write(result.content) 70 | 71 | self.response.out.write("Login via Twitter") 72 | 73 | def main(): 74 | application = webapp.WSGIApplication([('/(.*)', MainHandler)], 75 | debug=True) 76 | util.run_wsgi_app(application) 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | --------------------------------------------------------------------------------