Pages

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.


9 comments:

  1. Hi Alex, thanks for posting this code. I am just starting out with GAE, authentication, webapps etc and found this very easy to get up and running.

    But, I am having some trouble getting my head around how to extend this. As a "simple" test I am trying to access a Google Data feed after authenticating. I am sure that I am missing something very simple, but I cant figure out how to get the token (not the string that is displayed on the profile page) back so I can access the feed:

    class DocsHandler(BaseRequestHandler):
    def get(self):
    if self.logged_in:
    session = self.auth.get_user_by_session()
    token = session['token']
    client = gdata.spreadsheets.client.SpreadsheetsClient()

    --> #how to get the real token back ???
    --> token.authorize(client)

    This yields: AttributeError: 'unicode' object has no attribute 'authorize'

    I have gone through your code and all of the webapp2 docs, tried everything I could think of with User and UserToken, etc, and cant get anything to work. What am I missing here?

    cheers
    -gary

    ReplyDelete
  2. Hey Gary, I see what you mean. The session['token'] is a so-called request token needed to get an access token (I know, lots of "tokens", sorry).

    The access token is being passed in _on_signin() callback (see http://code.google.com/p/gae-simpleauth/source/browse/example/handlers.py#125), where auth_data argument would be something like this:

    {'access_token': 'ya29.AHES6ZRf23UHVi1V2.....', 'token_type': 'Bearer', 'expires_in': 3600, 'id_token': 'eyJhbGciOiJSUzI1N....a4BgMjRUW2Rs1038'}


    Now, if you're referring to http://code.google.com/p/gdata-python-client, you'll have instantiate their own OAuth2Token passing it the above access_token (you'll have to store it somewhere during e.g. _on_signin() so that you can use it later on):

    token = gdata.gauth. OAuth2Token(
    client_id='your registered app ID ',
    client_secret='your app secret',
    scope='scope to access google spreadsheet',
    user_agent='some ID, e.g. MyApp',
    access_token = access_token_from_the_above
    )

    token.authorize(client)

    OAuth2Token is defined here:
    http://code.google.com/p/gdata-python-client/source/browse/src/gdata/gauth.py#1110

    Also, see some alternative here:
    http://code.google.com/p/gdata-python-client/issues/detail?id=549

    Hope this helps!

    ReplyDelete
  3. Ah, OK... So all the pieces were there, I just couldnt figure out how to make them fit ;-) Thanks a lot for the guidance, this is very helpful to understanding how the whole process works.

    ReplyDelete
  4. Hi Alex, thanks a lot for making SimpleAuth. It's a great shortcut towards more user-friendly authentification!

    Obviously it's still possible that a user doesn't have (or doesn't want to use) any of the provided login methods. In that case it's probably a good idea to provide a simple username/password or email/password login as a fallback.

    How would you extend SimpleAuth to enable that?

    Of course it's possible to simply hack that functionality into the SimpleAuth flow, but you seem to know AppEngine and its APIs very well and I'd like to hear your opinion on what the best solution might be.

    Thanks!

    ReplyDelete
  5. Hey Tim, good point!

    The simple username/password auth is already there, in webapp2, but you're making a very good point and I'll definitely include this in the example and SimpleAuth itself.

    Will try get this done in the next days. I'll keep you posted.

    Thanks for the feedback!

    ReplyDelete
  6. Hi Alex,

    Love SimpleAuth - so easy to implement and yet so flexible. Would you have any tips for accessing a webapp with simpleauth from a command line prompt (posting using httplib etc.). It's wrecking my head :/

    Cheers!

    ReplyDelete
  7. Hey Stephen!
    If I'm understand it correctly, you might try doing it using the remote API, e.g.:

    PYTHONPATH=.:$PYTHONPATH /usr/local/google_appengine/remote_api_shell.py -s localhost:8080

    dev~example> import simpleauth
    dev~example> handler = simpleauth.SimpleAuthHandler()
    dev~example> auth_info = {'access_token':'my token'}
    dev~example> google_user_dict = handler._get_google_user_info(auth_info)

    You can basically call any method on that 'handler', which is defined starting from here:
    https://github.com/crhym3/simpleauth/blob/master/simpleauth/handler.py#L66


    Hope this helps! If not, we now have a google group :)
    https://groups.google.com/forum/#!forum/gae-simpleauth

    ReplyDelete
  8. Hi Alex,
    I look at SimpleAuth and I don't see password flow (aka resource owner password credentials grant) that Time suggested. My understanding is that webapp2.extensions.auth generates the access_token but then it stores it inside a session and session iside a cookie (not necessarily but by default). That is a problem to me as I try to implement API-only service for native mobile clients. I thought that all I need is a simple bearer access_token that client app receives from the server and then keeps including into a header. Do I get it wrong? Is it implemented in SimpleAuth and I just missed it?

    ReplyDelete
  9. Hey Michael. SimpleAuth doesn't do password flows but this post will clarify things: https://groups.google.com/d/msg/gae-simpleauth/kxpuYrc4ijY/bfELvvVJstQJ

    You get it right. Only that SimpleAuth doesn't provide an authentication mecanism for your app. It only does authentication with third party providers. Everything else is up to you.

    For instance, if you look at the example app, I'm using webapp2's built-in get_user_by_session() method (https://github.com/crhym3/simpleauth/blob/master/example/handlers.py#L47) which indeed uses cookies.

    For your API-only auth you could, for instance, parse an auth header, extract a token and then use get_user_by_token() method from webapp2_extras.auth: http://webapp-improved.appspot.com/api/webapp2_extras/auth.html#module-webapp2_extras.auth

    Hope this helps.

    ReplyDelete

What do you think?