"""
+import base64
import binascii
import cgi
import hashlib
from tornado import httpclient
from tornado import escape
+from tornado.ioloop import IOLoop
class OpenIdMixin(object):
"""Abstract implementation of OpenID and Attribute Exchange.
See TwitterMixin and FriendFeedMixin below for example implementations.
"""
- def authorize_redirect(self, callback_uri=None):
+
+ def authorize_redirect(self, callback_uri=None, extra_params=None):
"""Redirects the user to obtain OAuth authorization for this service.
Twitter and FriendFeed both require that you register a Callback
if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
raise Exception("This service does not support oauth_callback")
http = httpclient.AsyncHTTPClient()
- http.fetch(self._oauth_request_token_url(), self.async_callback(
- self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri))
+ if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+ http.fetch(self._oauth_request_token_url(callback_uri=callback_uri,
+ extra_params=extra_params),
+ self.async_callback(
+ self._on_request_token,
+ self._OAUTH_AUTHORIZE_URL,
+ callback_uri))
+ else:
+ http.fetch(self._oauth_request_token_url(), self.async_callback(
+ self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri))
+
def get_authenticated_user(self, callback):
"""Gets the OAuth authorized user and access token on callback.
attributes like 'name' includes the 'access_key' attribute, which
contains the OAuth access you can use to make authorized requests
to this service on behalf of the user.
+
"""
request_key = self.get_argument("oauth_token")
+ oauth_verifier = self.get_argument("oauth_verifier", None)
request_cookie = self.get_cookie("_oauth_request_token")
if not request_cookie:
logging.warning("Missing OAuth request token cookie")
callback(None)
return
- cookie_key, cookie_secret = request_cookie.split("|")
+ self.clear_cookie("_oauth_request_token")
+ cookie_key, cookie_secret = [base64.b64decode(i) for i in request_cookie.split("|")]
if cookie_key != request_key:
logging.warning("Request token does not match cookie")
callback(None)
return
token = dict(key=cookie_key, secret=cookie_secret)
+ if oauth_verifier:
+ token["verifier"] = oauth_verifier
http = httpclient.AsyncHTTPClient()
http.fetch(self._oauth_access_token_url(token), self.async_callback(
self._on_access_token, callback))
- def _oauth_request_token_url(self):
+ def _oauth_request_token_url(self, callback_uri= None, extra_params=None):
consumer_token = self._oauth_consumer_token()
url = self._OAUTH_REQUEST_TOKEN_URL
args = dict(
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version="1.0",
+ oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
)
- signature = _oauth_signature(consumer_token, "GET", url, args)
+ if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+ if callback_uri:
+ args["oauth_callback"] = urlparse.urljoin(
+ self.request.full_url(), callback_uri)
+ if extra_params: args.update(extra_params)
+ signature = _oauth10a_signature(consumer_token, "GET", url, args)
+ else:
+ signature = _oauth_signature(consumer_token, "GET", url, args)
+
args["oauth_signature"] = signature
return url + "?" + urllib.urlencode(args)
if response.error:
raise Exception("Could not get request token")
request_token = _oauth_parse_response(response.body)
- data = "|".join([request_token["key"], request_token["secret"]])
+ data = "|".join([base64.b64encode(request_token["key"]),
+ base64.b64encode(request_token["secret"])])
self.set_cookie("_oauth_request_token", data)
args = dict(oauth_token=request_token["key"])
if callback_uri:
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version="1.0",
+ oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
)
- signature = _oauth_signature(consumer_token, "GET", url, args,
- request_token)
+ if "verifier" in request_token:
+ args["oauth_verifier"]=request_token["verifier"]
+
+ if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+ signature = _oauth10a_signature(consumer_token, "GET", url, args,
+ request_token)
+ else:
+ signature = _oauth_signature(consumer_token, "GET", url, args,
+ request_token)
+
args["oauth_signature"] = signature
return url + "?" + urllib.urlencode(args)
logging.warning("Could not fetch access token")
callback(None)
return
+
access_token = _oauth_parse_response(response.body)
user = self._oauth_get_user(access_token, self.async_callback(
self._on_oauth_get_user, access_token, callback))
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version="1.0",
+ oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
)
args = {}
args.update(base_args)
args.update(parameters)
- signature = _oauth_signature(consumer_token, method, url, args,
- access_token)
+ if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+ signature = _oauth10a_signature(consumer_token, method, url, args,
+ access_token)
+ else:
+ signature = _oauth_signature(consumer_token, method, url, args,
+ access_token)
base_args["oauth_signature"] = signature
return base_args
+class OAuth2Mixin(object):
+ """Abstract implementation of OAuth v 2."""
+
+ def authorize_redirect(self, redirect_uri=None, client_id=None,
+ client_secret=None, extra_params=None ):
+ """Redirects the user to obtain OAuth authorization for this service.
+
+ Some providers require that you register a Callback
+ URL with your application. You should call this method to log the
+ user in, and then call get_authenticated_user() in the handler
+ you registered as your Callback URL to complete the authorization
+ process.
+ """
+ args = {
+ "redirect_uri": redirect_uri,
+ "client_id": client_id
+ }
+ if extra_params: args.update(extra_params)
+ self.redirect(self._OAUTH_AUTHORIZE_URL +
+ urllib.urlencode(args))
+
+ def _oauth_request_token_url(self, redirect_uri= None, client_id = None,
+ client_secret=None, code=None,
+ extra_params=None):
+ url = self._OAUTH_ACCESS_TOKEN_URL
+ args = dict(
+ redirect_uri=redirect_uri,
+ code=code,
+ client_id=client_id,
+ client_secret=client_secret,
+ )
+ if extra_params: args.update(extra_params)
+ return url + urllib.urlencode(args)
class TwitterMixin(OAuthMixin):
"""Twitter OAuth authentication.
_OAUTH_ACCESS_TOKEN_URL = "http://api.twitter.com/oauth/access_token"
_OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
_OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
- _OAUTH_NO_CALLBACKS = True
+ _OAUTH_NO_CALLBACKS = False
+
def authenticate_redirect(self):
"""Just like authorize_redirect(), but auto-redirects if authorized.
it is required to make requests on behalf of the user later with
friendfeed_request().
"""
+ _OAUTH_VERSION = "1.0"
_OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
_OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
_OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
_OAUTH_NO_CALLBACKS = True
+ _OAUTH_VERSION = "1.0"
+
def friendfeed_request(self, path, callback, access_token=None,
post_args=None, **args):
def _oauth_get_user(self, access_token, callback):
OpenIdMixin.get_authenticated_user(self, callback)
-
class FacebookMixin(object):
"""Facebook Connect authentication.
if isinstance(body, unicode): body = body.encode("utf-8")
return hashlib.md5(body).hexdigest()
+class FacebookGraphMixin(OAuth2Mixin):
+ _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
+ _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?"
+ _OAUTH_NO_CALLBACKS = False
+
+ def get_authenticated_user(self, redirect_uri, client_id, client_secret,
+ code, callback):
+ """ Handles the login for the Facebook user, returning a user object.
+
+ Example usage:
+ class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("code", False):
+ self.get_authenticated_user(
+ redirect_uri='/auth/facebookgraph/',
+ client_id=self.settings["facebook_api_key"],
+ client_secret=self.settings["facebook_secret"],
+ code=self.get_argument("code"),
+ callback=self.async_callback(
+ self._on_login))
+ return
+ self.authorize_redirect(redirect_uri='/auth/facebookgraph/',
+ client_id=self.settings["facebook_api_key"],
+ extra_params={"scope": "read_stream,offline_access"})
+
+ def _on_login(self, user):
+ logging.error(user)
+ self.finish()
+
+ """
+ http = httpclient.AsyncHTTPClient()
+ args = {
+ "redirect_uri": redirect_uri,
+ "code": code,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ }
+
+ http.fetch(self._oauth_request_token_url(**args),
+ self.async_callback(self._on_access_token, redirect_uri, client_id,
+ client_secret, callback))
+
+ def _on_access_token(self, redirect_uri, client_id, client_secret,
+ callback, response):
+ session = {
+ "access_token": cgi.parse_qs(response.body)["access_token"][-1],
+ "expires": cgi.parse_qs(response.body)["expires"]
+ }
+
+ self.facebook_request(
+ path="/me",
+ callback=self.async_callback(
+ self._on_get_user_info, callback, session),
+ access_token=session["access_token"],
+ fields="picture"
+ )
+
+
+ def _on_get_user_info(self, callback, session, user):
+ if user is None:
+ callback(None)
+ return
+ callback({
+ "name": user["name"],
+ "first_name": user["first_name"],
+ "last_name": user["last_name"],
+ "id": user["id"],
+ "locale": user["locale"],
+ "picture": user.get("picture"),
+ "link": user["link"],
+ "username": user.get("username"),
+ "access_token": session["access_token"],
+ "session_expires": session.get("expires"),
+ })
+
+ def facebook_request(self, path, callback, access_token=None,
+ post_args=None, **args):
+ """Fetches the given relative API path, e.g., "/btaylor/picture"
+
+ If the request is a POST, post_args should be provided. Query
+ string arguments should be given as keyword arguments.
+
+ An introduction to the Facebook Graph API can be found at
+ http://developers.facebook.com/docs/api
+
+ Many methods require an OAuth access token which you can obtain
+ through authorize_redirect() and get_authenticated_user(). The
+ user returned through that process includes an 'access_token'
+ attribute that can be used to make authenticated requests via
+ this method. Example usage:
+
+ class MainHandler(tornado.web.RequestHandler,
+ tornado.auth.FacebookGraphMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def get(self):
+ self.facebook_request(
+ "/me/feed",
+ post_args={"message": "I am posting from my Tornado application!"},
+ access_token=self.current_user["access_token"],
+ callback=self.async_callback(self._on_post))
+
+ def _on_post(self, new_entry):
+ if not new_entry:
+ # Call failed; perhaps missing permission?
+ self.authorize_redirect()
+ return
+ self.finish("Posted a message!")
+
+ """
+ url = "https://graph.facebook.com" + path
+ all_args = {}
+ if access_token:
+ all_args["access_token"] = access_token
+ all_args.update(args)
+ all_args.update(post_args or {})
+ if all_args: url += "?" + urllib.urlencode(all_args)
+ callback = self.async_callback(self._on_facebook_request, callback)
+ http = httpclient.AsyncHTTPClient()
+ if post_args is not None:
+ http.fetch(url, method="POST", body=urllib.urlencode(post_args),
+ callback=callback)
+ else:
+ http.fetch(url, callback=callback)
+
+ def _on_facebook_request(self, callback, response):
+ if response.error:
+ logging.warning("Error response %s fetching %s", response.error,
+ response.request.url)
+ callback(None)
+ return
+ callback(escape.json_decode(response.body))
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
"""Calculates the HMAC-SHA1 OAuth signature for the given request.
hash = hmac.new(key, base_string, hashlib.sha1)
return binascii.b2a_base64(hash.digest())[:-1]
+def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None):
+ """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
+
+ See http://oauth.net/core/1.0a/#signing_process
+ """
+ parts = urlparse.urlparse(url)
+ scheme, netloc, path = parts[:3]
+ normalized_url = scheme.lower() + "://" + netloc.lower() + path
+
+ base_elems = []
+ base_elems.append(method.upper())
+ base_elems.append(normalized_url)
+ base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
+ for k, v in sorted(parameters.items())))
+
+ base_string = "&".join(_oauth_escape(e) for e in base_elems)
+ key_elems = [urllib.quote(consumer_token["secret"], safe='~')]
+ key_elems.append(urllib.quote(token["secret"], safe='~') if token else "")
+ key = "&".join(key_elems)
+
+ hash = hmac.new(key, base_string, hashlib.sha1)
+ return binascii.b2a_base64(hash.digest())[:-1]
def _oauth_escape(val):
if isinstance(val, unicode):
special = ("oauth_token", "oauth_token_secret")
token.update((k, p[k][0]) for k in p if k not in special)
return token
+
+