Sunday, February 12, 2012

Simple auth on App Engine without passwords

UPDATE(24 Feb, 2013) - looks like many people started using it! so, I created a google group:
https://groups.google.com/forum/#!forum/gae-simpleauth


This post is about how to throw away some passwords and give users a better experience for those who's developing an app on Google App Engine in python.

Every time I start writing a new app, one of the most frequent questions popping up is, "how do we handle users login?"

The thing is, I'm getting really tired of handling username and passwords within an app. Not because it's tough. Every person on earth almost literally already have an Google, Facebook, LinkedIn or other network account. Why do we still creating registration forms? In fact, I'm becoming sort of allergic to "registration forms".

Almost every (social) network provides API for authentication that support at least one of the following: OAuth 2.0, OAuth 1.0 or OpenID.

So, in a couple past days I went on and checked out existing libraries for mentioned specs and I didn't really liked what I found. A lot of them have an "all or nothing" approach.

For instance, there's Google APIs Client Library for Python but it does so much more besides a simple authentication for your app. I agree, this is a nice library if your app is based on top of activities and other Google+ stuff. But, what if you just wanted a simple authentication?

Or, take EngineAuth. The demo site is awesome, but if you look at the code, lots of stuff that already exist in the underlying framework (webapp2) was rewritten, which by the way enforces you to use a predefined base of a user model.

What I'm saying is, in a scenario where you want users to be able to just login with their preferred accounts, including at least Twitter, Facebook, Google, LinkedIn, Windows Live and OpenID, you'll end up using a small percent of each existing library that you have to include in order to suppor mentioned providers. Basically, it'll instantly blow up the size of your app. It means more runtime memory consumption, disk space, maintenance, etc.

So, I ended up writing if from scratch. I called it SimpleAuth.

What I had in mind while writing the code, is I didn't want to enforce any base model or request handler. Basically, it is a simple python object that can handle OAuth and OpenID requests, plus it also knows how to get a base profile data of a user.

Here's an example of how your webapp2 handler could look like using SimpleAuth:

class AuthHandler(webapp2.RequestHandler, SimpleAuthHandler):
  """Authentication handler for all kinds of auth."""

  def _on_signin(self, data, auth_info, provider):
    """Callback whenever a new or existing user is logging in.
    data is a user info dictionary.
    auth_info contains access token or oauth token and secret.

    See what's in it with logging.info(data, auth_info)
    """

    auth_id = auth_info['id']

    # 1. check whether user exist, e.g.
    #    User.get_by_auth_id(auth_id)
    #
    # 2. create a new user if it doesn't
    #    User(**data).put()
    #
    # 3. sign in the user
    #    self.session['_user_id'] = auth_id
    #
    # 4. redirect somewhere, e.g. self.redirect('/profile')
    #
    # See more on how to work the above steps here:
    # http://webapp-improved.appspot.com/api/webapp2_extras/auth.html
    # http://code.google.com/p/webapp-improved/issues/detail?id=20
   
  def logout(self):
    self.auth.unset_session()
    self.redirect('/')

  def _callback_uri_for(self, provider):
    return self.uri_for('auth_callback', provider=provider, _full=True)

  def _get_consumer_info_for(self, provider):
    """Should return a tuple (key, secret) for auth init requests.
    For OAuth 2.0 you should also return a scope, e.g.
    ('my app id', 'my app secret', 'email,user_about_me')
   
    The scope depens solely on the provider.
    See example/secrets.py.template
    """
    return secrets.AUTH_CONFIG[provider]

I define a single callback method, "_on_signin()" what will get called (by SimpleAuthHandler) whenever a user is trying to sign in.

The method is being passed 3 argumens:
  • data - a dictionary with user profile info. For instance, Google would give you this:
{
  "id": "114517983826182834234",
  "email": "alex@cloudware.it",
  "verified_email": true,
  "name": "Alex Vagin",
  "given_name": "Alex",
  "family_name": "Vagin",
  "link": "https://plus.google.com/114517983826182834234",
  "picture": "https://lh6.googleusercontent.com/-e9YCrBgmW5I/AAAAAAAAAAI/AAAAAAAAFV0/xCEiulUyHRc/photo.jpg",
  "gender": "male",
  "locale": "en"
}

  • auth_info is a dictionary containing access token (for OAuth 2) or token and a secret (OAuth 1.0). An example of OAuth2 auth_info:
{
  "client_secret": "E7ueb........UYBH1",
  "code": u"kACAH-1Ng3eOcakGO0JdWijf5v2g......M-QHfREvsR_7Jx_hoMoB",
  "grant_type": "authorization_token",
  "client_id": "958.....10.apps.googleusercontent.com",
  "redirect_uri": "http://simpleauth.appspot.com/auth/google/callback"
}

  • provider - a string indicating which provider current user's coming from. It can be one of the following: "google", "facebook", "linkedin", "twitter", "windows_live", "openid".
There's a working example app in the source code of SimpleAuth repo on GitHub

Deployed version of the example is here:

Logging in with any of the presented providers will get you to a simple profile page with your photo, name and a link:


If you like it, the best approach though, is to look at SimpleAuthHandler source code.

Let me know what you think!


PS the SimpleAuth is also available on PyPI. So, you can simply do
pip install simpleauth.