Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add LinkedInMixin - Send oauth_version 1.0 instead of 1.0a #236

Closed
wants to merge 1 commit into from

6 participants

@coopernurse

Add LinkedInMixin to auth module. If OAUTH_VERSION is 1.0a, send 1.0 per spec. Small cleanups to pass pyflakes.

@seanvoss

Any chance this is going to make it in?

Been about 6months with no feedback, wanted to use the main instead of the fork, and I'm going to use this functionality.

@jparise

I just gave this a spin, and it's working fine for me.

This new code does perpetuate some obsolete patterns (i.e. async_callback), but that's true of the tornado.auth code in general.

@jparise jparise commented on the diff
tornado/auth.py
@@ -270,7 +269,7 @@ def _oauth_request_token_url(self, callback_uri= None, extra_params=None):
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+ oauth_version=self._oauth_version_to_send()
@jparise
jparise added a note

Shouldn't this always be "1.0"? The rest of the tornado.auth code is pretty much hardwired to handle either version 1.0 or 1.0a, and if we always need to send 1.0 in the 1.0a case, all cases are covered.

This also makes the _oauth_version_to_send() method unnecessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@torsionpeg

Is LinkedinMixIn going to make it in, anytime soon ?

@bdarnell
Owner

No, I don't intend to add any more one-off auth classes to Tornado. Anyone who's interested can maintain one or more of these kinds of classes as a separate package (e.g. my own DropboxMixin at https://github.com/bdarnell/async_dropbox). I've left this pull request for the _oauth_version_to_send part, but I've never gotten around to researching the right thing to do here.

@bdarnell bdarnell closed this pull request from a commit
@bdarnell bdarnell Always send oauth_version=1.0, even when using 1.0a.
This is required by the spec (http://oauth.net/core/1.0/#auth_step1).
Many providers (including Google and Twitter) allow a value of either
1.0 or 1.0a here, but e.g. LinkedIn requires 1.0.

Closes #236.
6e00a75
@bdarnell bdarnell closed this in 6e00a75
@ysimonson

FYI for the sake of posterity, this class breaks from changes somewhere between tornado 3.0.1 and 3.1. We were using this code until the upgrade to 3.1, but now get this error:

TypeError: _on_request_token() takes exactly 5 arguments (4 given)

Now that LinkedIn supports OAuth2 (not the case when this patch was submitted 2 years ago), people should probably build off OAuth2Mixin anyway.

@ysimonson

Also, I made a LinkedIn OAuth2 implementation, based on a combination of this implementation and the Facebook graph mixin: https://gist.github.com/ysimonson/5877284

We don't use it in production due to historic reasons, so it's largely untested.

@bdarnell bdarnell referenced this pull request
Open

OAuth2 #1212

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 27, 2011
  1. @coopernurse

    Add LinkedInMixin to auth module. If OAUTH_VERSION is 1.0a, send 1.0 …

    coopernurse authored
    …per spec. Small cleanups to pass pyflakes.
This page is out of date. Refresh to see the latest.
Showing with 114 additions and 11 deletions.
  1. +114 −11 tornado/auth.py
View
125 tornado/auth.py
@@ -56,9 +56,8 @@ def _on_auth(self, user):
import urlparse
import uuid
-from tornado import httpclient
+from tornado import httpclient, httputil
from tornado import escape
-from tornado.ioloop import IOLoop
class OpenIdMixin(object):
"""Abstract implementation of OpenID and Attribute Exchange.
@@ -270,7 +269,7 @@ def _oauth_request_token_url(self, callback_uri= None, extra_params=None):
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+ oauth_version=self._oauth_version_to_send()
@jparise
jparise added a note

Shouldn't this always be "1.0"? The rest of the tornado.auth code is pretty much hardwired to handle either version 1.0 or 1.0a, and if we always need to send 1.0 in the 1.0a case, all cases are covered.

This also makes the _oauth_version_to_send() method unnecessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
)
if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
if callback_uri:
@@ -306,7 +305,7 @@ def _oauth_access_token_url(self, request_token):
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+ oauth_version=self._oauth_version_to_send()
)
if "verifier" in request_token:
args["oauth_verifier"]=request_token["verifier"]
@@ -328,7 +327,7 @@ def _on_access_token(self, callback, response):
return
access_token = _oauth_parse_response(response.body)
- user = self._oauth_get_user(access_token, self.async_callback(
+ self._oauth_get_user(access_token, self.async_callback(
self._on_oauth_get_user, access_token, callback))
def _oauth_get_user(self, access_token, callback):
@@ -355,7 +354,7 @@ def _oauth_request_parameters(self, url, access_token, parameters={},
oauth_signature_method="HMAC-SHA1",
oauth_timestamp=str(int(time.time())),
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
- oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+ oauth_version=self._oauth_version_to_send(),
)
args = {}
args.update(base_args)
@@ -369,6 +368,11 @@ def _oauth_request_parameters(self, url, access_token, parameters={},
base_args["oauth_signature"] = signature
return base_args
+ def _oauth_version_to_send(self):
+ v = getattr(self, "_OAUTH_VERSION", "1.0")
+ if v == "1.0a": v = "1.0"
+ return v
+
class OAuth2Mixin(object):
"""Abstract implementation of OAuth v 2."""
@@ -498,7 +502,6 @@ def _on_post(self, new_entry):
all_args = {}
all_args.update(args)
all_args.update(post_args or {})
- consumer_token = self._oauth_consumer_token()
method = "POST" if post_args is not None else "GET"
oauth = self._oauth_request_parameters(
url, access_token, all_args, method=method)
@@ -572,7 +575,6 @@ def _on_auth(self, user):
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"
@@ -621,7 +623,6 @@ def _on_post(self, new_entry):
all_args = {}
all_args.update(args)
all_args.update(post_args or {})
- consumer_token = self._oauth_consumer_token()
method = "POST" if post_args is not None else "GET"
oauth = self._oauth_request_parameters(
url, access_token, all_args, method=method)
@@ -684,8 +685,10 @@ def get(self):
def _on_auth(self, user):
if not user:
raise tornado.web.HTTPError(500, "Google auth failed")
- # Save the user with, e.g., set_secure_cookie()
-
+ else:
+ # Save the user with, e.g., set_secure_cookie()
+ self.finish("got user: <pre>%s</pre>" % pprint.pformat(user))
+
"""
_OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
_OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
@@ -1077,6 +1080,106 @@ def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
hash = hmac.new(key, base_string, hashlib.sha1)
return binascii.b2a_base64(hash.digest())[:-1]
+class LinkedInMixin(OAuthMixin):
+ """
+ LinkedIn oauth implementation. To use, set two settings in your tornado application:
+
+ app_settings = {
+ 'linkedin_consumer_key' : 'abc123',
+ 'linkedin_consumer_secret' : 'zzzzzzz'
+ }
+ application = tornado.web.Application([
+ (r"/login", handlers.LoginHandler)
+ ], **app_settings)
+
+ Then in your handler:
+
+ class LoginHandler(tornado.web.RequestHandler, auth.LinkedInMixin):
+
+ @tornado.web.asynchronous
+ def get(self):
+ # you can optionally change the fields set on the user after login
+ # see LinkedIn's docs for field list:
+ #
+ # http://developer.linkedin.com/docs/DOC-1002
+ #
+ #self._DEFAULT_USER_FIELDS = "(id,first-name,last-name, headline,summary)"
+ if self.get_argument("oauth_token", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authorize_redirect(callback_uri=self.request.uri)
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "LinkedIn auth failed")
+ else:
+ # Save the user using, e.g., set_secure_cookie()
+ self.finish("got user: <pre>%s</pre>" % pprint.pformat(user))
+
+ """
+ _OAUTH_REQUEST_TOKEN_URL = "https://api.linkedin.com/uas/oauth/requestToken"
+ _OAUTH_ACCESS_TOKEN_URL = "https://api.linkedin.com/uas/oauth/accessToken"
+ _OAUTH_AUTHORIZE_URL = "https://www.linkedin.com/uas/oauth/authorize"
+ _OAUTH_AUTHENTICATE_URL = "https://www.linkedin.com/uas/oauth/authenticate"
+ _OAUTH_VERSION = "1.0a"
+ _OAUTH_NO_CALLBACKS = False
+ _DEFAULT_USER_FIELDS = "(id,first-name,last-name,headline,industry," + \
+ "positions,educations,summary,picture-url)"
+
+ def authenticate_redirect(self):
+ """Just like authorize_redirect(), but auto-redirects if authorized.
+
+ This is generally the right interface to use if you are using
+ LinkedIn for single-sign on.
+ """
+ http = httpclient.AsyncHTTPClient()
+ http.fetch(self._oauth_request_token_url(), self.async_callback(
+ self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
+
+ def linkedin_request(self, path, callback, access_token=None, post_args=None, **args):
+ url = "http://api.linkedin.com" + path
+ if access_token:
+ all_args = {}
+ all_args.update(args)
+ all_args.update(post_args or {})
+ method = "POST" if post_args is not None else "GET"
+ oauth = self._oauth_request_parameters(
+ url, access_token, all_args, method=method)
+ args.update(oauth)
+ if args: url += "?" + urllib.urlencode(args)
+ callback = self.async_callback(self._on_linkedin_request, callback)
+ http = httpclient.AsyncHTTPClient()
+ # ask linkedin to send us JSON on all API calls (not xml)
+ headers = httputil.HTTPHeaders({"x-li-format":"json"})
+ if post_args is not None:
+ http.fetch(url, method="POST", headers=headers, body=urllib.urlencode(post_args),
+ callback=callback)
+ else:
+ http.fetch(url, headers=headers, callback=callback)
+
+ def _parse_user_response(self, callback, user):
+ callback(user)
+
+ def _on_linkedin_request(self, callback, response):
+ if response.error:
+ logging.warning("Error response %s fetching %s", response.error,
+ response.request.url)
+ callback(None)
+ else:
+ callback(escape.json_decode(response.body))
+
+ def _oauth_consumer_token(self):
+ self.require_setting("linkedin_consumer_key", "LinkedIn OAuth")
+ self.require_setting("linkedin_consumer_secret", "LinkedIn OAuth")
+ return dict(
+ key=self.settings["linkedin_consumer_key"],
+ secret=self.settings["linkedin_consumer_secret"])
+
+ def _oauth_get_user(self, access_token, callback):
+ callback = self.async_callback(self._parse_user_response, callback)
+ self.linkedin_request("/v1/people/~:%s" % self._DEFAULT_USER_FIELDS,
+ access_token=access_token, callback=callback)
+
def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None):
"""Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
Something went wrong with that request. Please try again.