Using OpenID, OAuth, OAuth2 and OpenID+OAuth

Over the last year I’ve had an authentication library that I’ve used to slice and dice public services and like most things it’s collected more than it’s share of dust, cruft and other ugly appendages that you wonder if it’ll work then next time you use it.  I’ve been hot and heavy over django (even if it’s embedded inside of Tornado) as a general framework for a while, it’s not broke don’t fix it…

The general case of site authentication looks like this:

  • You need your own username + password
  • You’re perfectly willing to give it all to Facebok/Google/etc. to handle

Depeding on the project I’m quite happy with giving it away, but there are times when you want to have “ownership” of the users on your website.  In which case in this day and age it’s important to allow people to associate their well known credentials with your service — cool.  FYI – This is my “new favorite flow”

  • Ask for email + password + (any service required fields – screen name ….)
  • Require them to associate with  another service
    • Capture picture / full name and other bits from “Facebook”
  • Follow up with prompting to finish profile or other service specific tasks

That’s the simple part, the hard part has been dealing with OpenID, OAuth, OAuth2 and Hybrid protocols.   Since very rarely do you want just to get the fine photo for the user and forget about it.  You probably want to do one of these things:

  • Tweet something they did
  • Check in
  • Add to their facebook page
  • Scrape their friends
  • …etc…

Which means you need to store a token, not only that but some of these wonderful protocols don’t give you persistant identifiers.   Anyway, here’s a bit of commentary and some code excerpts for you to review, hopefully my refactorization makes life more interesting moving forward and those hulking if statements I used to have are gone.

OAuth — The big challenge is that the token you get is a transient identifier, it will change if you assocate account information with this your doomed.   So, typically what you end up needing to do is take your OAuth token and go back and pull the profile, which of course means that you need yet another round trip behind the scenes to get an authentication to happen.

class GowallaBackend(OpenBackend) :
    name = gowalla
    info = settings.OPENAUTH_DATA.get(name, {})

    def prompt(self) :
        return https://api.gowalla.com/api/oauth/new?%s % urllib.urlencode({
            client_id : self.key,
            redirect_uri : self.return_to,
        })

    def get_access_token(self) :
        url = https://api.gowalla.com/api/oauth/token
        body = {
            client_id : self.key,
            client_secret : self.secret,
            redirect_uri : self.return_to,
            code : self.request.GET[code],
            grant_type : authorization_code,
        }

        data = self._fetch(url, postdata=body, headers={Accept:application/json})

        vals = json.loads(data)

        return OpenToken(vals[access_token])

    def get_profile(self, token) :
        from ..libs.gowalla import Gowalla

        go = Gowalla(self.key, access_token=token.token)

        profile = go.user_me()

        identity = "gowalla:%s" % profile[url]

        return identity, {
            first_name : profile[first_name],
            last_name : profile[last_name],
            email : None,
        }, profile

OAuth+OpenID — This is where life gets a bit more painful…  Typically over getting all of the URI bits worked out, where things go etc.  We won’t mention strange things like Yahoo doesn’t return the oauth token when asked unless you’ve approved yourself for non-public information…  Gack!

class GoogleBackend(OpenBackend) :
    name = google
    info = settings.OPENAUTH_DATA.get(name,{})

    def _get_client(self) :
        client = consumer.Consumer(self.request.session, util.OpenIDStore())
        client.setAssociationPreference([(HMAC-SHA1, no-encryption)])
        return client

    def prompt(self) :
        client = self._get_client()

        auth_request = client.begin(https://www.google.com/accounts/o8/id)

        auth_request.addExtensionArg(http://openid.net/srv/ax/1.0, mode, fetch_request)
        auth_request.addExtensionArg(http://openid.net/srv/ax/1.0, required, email,firstname,lastname)
        auth_request.addExtensionArg(http://openid.net/srv/ax/1.0, type.email, http://schema.openid.net/contact/email)
        auth_request.addExtensionArg(http://openid.net/srv/ax/1.0, type.firstname, http://axschema.org/namePerson/first)
        auth_request.addExtensionArg(http://openid.net/srv/ax/1.0, type.lastname, http://axschema.org/namePerson/last)

        auth_request.addExtensionArg(http://specs.openid.net/extensions/oauth/1.0, consumer, self.key)
        auth_request.addExtensionArg(http://specs.openid.net/extensions/oauth/1.0, scope, http://www.google.com/m8/feeds)

        parts = list(urlparse.urlparse(self.return_to))
        realm = urlparse.urlunparse(parts[0:2] + [] * 4)

        return auth_request.redirectURL(realm, self.return_to)

    def get_access_token(self) :
        if self.request.GET.get(openid.mode, None) == cancel or self.request.GET.get(openid.mode, None) != id_res :
            raise OpenBackendDeclineException()

        client = self._get_client()
        auth_response = client.complete(self.request.GET, self.return_to)

        if isinstance(auth_response, consumer.FailureResponse) :
            raise OpenBackendDeclineException("%s" % auth_response)

        ax = auth_response.extensionResponse(http://openid.net/srv/ax/1.0, True)

        self.email = ax.get(value.email,)
        self.first_name = ax.get(value.firstname,)
        self.last_name = ax.get(value.lastname,)

        self.identity = auth_response.getSigned(openid.message.OPENID2_NS, identity, None)

        otoken = auth_response.extensionResponse(http://specs.openid.net/extensions/oauth/1.0, True)
        oclient = GoogleOAuthClient(self.key, self.secret)

        tok = oclient.get_access_token(otoken[request_token])
        return OpenToken(tok[oauth_token], tok[oauth_token_secret])

    def get_profile(self, token) :
        v = {
            email : self.email,
            first_name : self.first_name,
            last_name : self.last_name,
        }
        return self.identity, v, v