Features

  • Loosely coupled.
  • Tiny but powerful interface.
  • The python-openid library is the only optional dependency.
  • CSRF protection.
  • Framework agnostic thanks to adapters. Out of the box support for Django, Flask, Pyramid and Webapp2.
  • Ready to accommodate future authorization / authentication protocols.
  • Makes calls to provider APIs a breeze.
  • Supports asynchronous requests.
  • JavaScript library as a bonus.
  • Out of the box support for:
    • OAuth 1.0a providers: Bitbucket, Flickr, Meetup, Plurk, Twitter, Tumblr, UbuntuOne, Vimeo, Xero, Xing and Yahoo.
    • OAuth 2.0 providers: Amazon, Behance, Bitly, Cosm, DeviantART, Eventbrite, Facebook, Foursquare, GitHub, Google, LinkedIn, PayPal, Reddit, Viadeo, VK, WindowsLive, Yammer and Yandex.
    • python-openid and Google App Engine based OpenID.

License and Requirements

The package is licensed under MIT license and requires Python 2.6 and higher, but doesn’t support Python 3 yet.

Live Demo

There is a Google App Engine based live demo app running at http://authomatic-example.appspot.com which makes use of most of the features.

Contribute

Stories in Ready

Contributions of any kind are very welcome. If you want to contribute, please read the Development Guide first. The project is hosted on GitHub.

If you find this library useful and are using it in your projects, please don’t be shy and leave a comment about your use case on the Authomatic use cases issue.

Usage

So you want your app to be able to log a user in with Facebook, Twitter, OpenID or whatever? First install Authomatic through PyPi,

$ pip install authomatic

or clone it from GitHub.

$ git clone git://github.com/peterhudec/authomatic.git

Note

On Google App Engine you need to include the authomatic module or a link to it inside your app’s directory.

Now it’s dead simple (hence the Deadsimpleauth). Just go through these two steps:

  1. Make an instance of the Authomatic class.
  2. Log the user in by calling the Authomatic.login() method inside a request handler.

Note

The interface of the library has recently been changed from:

import authomatic
authomatic.setup(CONFIG, 'secret')

to more flexible:

from authomatic import Authomatic
authomatic = Authomatic(CONFIG, 'secret')

The old interface will be availabe up to version 0.1.0, but you will recieve deprecation warnings in the log.

If everything goes good, you will get a User object with information like User.name, User.id or User.email. Moreover, if the user has logged in with an OAuth 2.0 or OAuth 1.0a provider, you will be able to access his/her protected resources.

Instantiate Authomatic

You need to pass a Config dictionary and a random secret string used for session signing and CSRF token generation to the constructor of the Authomatic class.

# main.py

import webapp2
from authomatic import Authomatic
from authomatic.adapters import Webapp2Adapter

from config import CONFIG

authomatic = Authomatic(config=CONFIG, secret='some random secret string')

The Config is a dictionary in which you configure Providers you want to use in your app. The keys are your internal provider names and values are dictionaries specifying configuration for a particular provider name.

Choose a particular provider by assigning a provider class to the "class_" key of the nested configuration dictionary. All the other keys are just keyword arguments, which will be passed to the chosen provider class constructor.

In this sample config we specify that Facebook will be available under the "fb" slug, Twitter under "tw", OpenID under "oi" and Google App Engine OpenID under "gae_oi":

# config.py

from authomatic.providers import oauth2, oauth1, openid, gaeopenid

CONFIG = {
    
    'tw': { # Your internal provider name
           
        # Provider class
        'class_': oauth1.Twitter,
        
        # Twitter is an AuthorizationProvider so we need to set several other properties too:
        'consumer_key': '########################',
        'consumer_secret': '########################',
    },
    
    'fb': {
           
        'class_': oauth2.Facebook,
        
        # Facebook is an AuthorizationProvider too.
        'consumer_key': '########################',
        'consumer_secret': '########################',
        
        # But it is also an OAuth 2.0 provider and it needs scope.
        'scope': ['user_about_me', 'email', 'publish_stream', 'read_stream'],
    },
    
    'gae_oi': {
           
        # OpenID provider based on Google App Engine Users API.
        # Works only on GAE and returns only the id and email of a user.
        # Moreover, the id is not available in the development environment!
        'class_': gaeopenid.GAEOpenID,
    },
    
    'oi': {
           
        # OpenID provider based on the python-openid library.
        # Works everywhere, is flexible, but requires more resources.
        'class_': openid.OpenID,
    }
}

Log the User In

Now you can log the user in by calling the authomatic.login() function inside a request handler. The request handler MUST be able to recieve both GET and POST HTTP methods. You need to pass it an adapter for your framework and one of the provider names which you specified in the keys of your Config. We will get the provider name from the URL slug.

class Login(webapp2.RequestHandler):
    def any(self, provider_name):
        result = authomatic.login(Webapp2Adapter(self), provider_name)

The authomatic.login() function will redirect the user to the provider, which will prompt him/her to authorize your app (the consumer) to access his/her protected resources (OAuth 1.0a and OAuth 2.0), or to verify his/her claimed ID (OpenID). The provider then redirects the user back to this request handler.

If the login procedure is over, authomatic.login() returns a LoginResult. You can check for errors in LoginResult.error or in better case for a User in LoginResult.user. The User object has plenty of useful properties.

Warning

Do not write anything to the response unless the login procedure is over! The authomatic.login() either returns None, which means that the login procedure si still pending, or a LoginResult which means that the login procedure is over.

Check whether login procedure is over.

        if result:
            self.response.write('<a href="..">Home</a>')

Check for errors, but hope that there is a LoginResult.user. If so, we have an authenticated user logged in. Before we print a welcoming message we need to update the User to get more info about him/her.

            if result.error:
                self.response.write(u'<h2>Damn that error: {}</h2>'.format(result.error.message))
            elif result.user:
                if not (result.user.name and result.user.id):
                    result.user.update()
                self.response.write(u'<h1>Hi {}</h1>'.format(result.user.name))
                self.response.write(u'<h2>Your id is: {}</h2>'.format(result.user.id))
                self.response.write(u'<h2>Your email is: {}</h2>'.format(result.user.email))

Advanced

Logging a user in is nice, but you can do more.

You can use the user’s credentials to access his/her protected resources, make asynchronous requests, use your own session implementation and Save your backend’s resources by utilizing the authomatic.js javascript library.

Credentials

If the User has User.credentials, he/she is logged in either with OAuth 1.0a or OAuth 2.0, both of which are subclasses of AuthorizationProvider. That means, that we can access the user’s protected resources. Lets get the user’s five most recent facebook statuses.

                if result.user.credentials:
                    if result.provider.name == 'fb':
                        self.response.write('Your are logged in with Facebook.<br />')
                        url = 'https://graph.facebook.com/{}?fields=feed.limit(5)'
                        url = url.format(result.user.id)
                        response = result.provider.access(url)

The call returns a Response object. The Response.data contains the parsed response content.

                        if response.status == 200:
                            statuses = response.data.get('feed').get('data')
                            error = response.data.get('error')
                            
                            if error:
                                self.response.write(u'Damn that error: {}!'.format(error))
                            elif statuses:
                                self.response.write('Your 5 most recent statuses:<br />')
                                for message in statuses:
                                    
                                    text = message.get('message')
                                    date = message.get('created_time')
                                    
                                    self.response.write(u'<h3>{}</h3>'.format(text))
                                    self.response.write(u'Posted on: {}'.format(date))
                        else:
                            self.response.write('Damn that unknown error!<br />')
                            self.response.write(u'Status: {}'.format(response.status))

Credentials can be serialized to a lightweight url-safe string.

serialized_credentials = credentials.serialize()

It would be useless if they could not be deserialized back to original.

Note

The deserialization of the credentials is dependent on the Config used when the credentials have been serialized. You can deserialize them in a different application as long as you use the same Config.

credentials = authomatic.credentials(serialized_credentials)

They know the provider name which you specified in the Config.

provider_name = credentials.provider_name

OAuth 2.0 credentials have limited lifetime. You can check whether they are still valid, in how many seconds they expire, get the date and time or UNIX timestamp of their expiration and find out whether they expire soon.

valid = credentials.valid # True / False
seconds_remaining = credentials.expire_in
expire_on = credentials.expiration_date # datetime.datetime()
expire_on = credentials.expiration_time # 1362080855
should_refresh = credentials.expire_soon(60 * 60 * 24) # True if expire in less than one day

You can refresh the credentials while they are still valid. Otherwise you must repeat the authomatic.login() procedure to get new credentials.

if credentials.expire_soon():
   response = credentials.refresh()
   if response and response.status == 200:
      print 'Credentials have been refreshed successfully.'

Finally use the credentials (serialized or deserialized) to access protected resources of the user to whom they belong by passing them to the authomatic.access() function along with the resource URL.

response = authomatic.access(credentials, 'https://graph.facebook.com/{id}?fields=birthday')

You can find out more about Credentials in the Reference. There is also a short tutorial about credentials in the Tutorials / Examples section.

Asynchronous Requests

Following functions fetch remote URLs and block the current thread till they return a Response.

If you need to call more than one of them in a single request handler, or if there is another time consuming task you need to do, there is an asynchronous alternative to each of these functions.

Warning

The internal implementation of the future pattern is quite naive. Use with caution!

These asynchronous alternatives all return a Future instance which represents the separate thread in which their synchronous brethren are running. You should call all the asynchronous functions you want to use at once, then do your time consuming tasks and finally collect the results of the functions by calling the get_result() method of each of the Future instances.

# These guys will run in parallel and each returns immediately.
user_future = user.async_update()
credentials_future = user.credentials.async_refresh()
foo_future = authomatic.access(user.credentials, 'http://api.example.com/foo')
bar_future = authomatic.access(user.credentials, 'http://api.example.com/bar')

# Do your time consuming task.
time.sleep(5)

# Collect results:

# Updates the User instance in place and returns response.
user_response = user_future.get_result()
if user_response.status == 200:
   print 'User was updated successfully.'

# Refreshes the Credentials instance in place and returns response.
credentials_response = credentials_future.get_result()
if credentials_response.status == 200:
   print 'Credentials were refreshed successfully.'

foo_response = foo_future.get_result()
bar_response = bar_future.get_result()

Session

The authomatic.login() function uses a default secure cookie based session to store state during the login procedure. If you want to use different session implementation you can pass it together with its save method to the authomatic.login() function. The only requirement is that the session implementation must have a dictionary-like interface.

Note

The default secure cookie based session will be deleted immediately after the login procedure is over. Custom sessions however, will be preserved.

import webapp2
from webapp2_extras import sessions
import authomatic
from authomatic.adapters import Webapp2Adapter

class Login(webapp2.RequestHandler):
   def any(self, provider_name):

      # Webapp2 session
      session_store = sessions.get_store(request=self.request)
      session = session_store.get_session()
      session_saver = lambda: session_store.save_sessions(self.response)

      result = authomatic.login(Webapp2Adapter(self),
                                provider_name,
                                session=session,
                                session_saver=session_saver)

Man, isn’t there a simpler way to make a Webapp2 session? You guessed it didn’t you? There is one in the authomatic.extras.gae module:

import webapp2
import authomatic
from authomatic.adapters import Webapp2Adapter
from authomatic.extras import gae

class Login(webapp2.RequestHandler):
   def any(self, provider_name):

      # Creates a new Webapp2 session.
      session = gae.Webapp2Session(self, secret='your-super-confidential-secret')

      result = authomatic.login(Webapp2Adapter(self),
                                provider_name,
                                session=session,
                                session_saver=session.save)

If you are already using a Webapp2 session you can do it like this:

import webapp2
import authomatic
from authomatic.adapters import Webapp2Adapter
from authomatic.extras import gae

class Login(webapp2.RequestHandler):
   def any(self, provider_name):

      # Wraps an existing Webapp2 session.
      session = gae.Webapp2Session(self, session=self.session)

      result = authomatic.login(Webapp2Adapter(self),
                                provider_name,
                                session=session,
                                session_saver=session.save)

JavaScript

Access

Accessing the user’s protected resources and provider APIs is very easy thanks to the authomatic.access() function, but you could save your backend’s resources by delegating it to the user’s browser.

This however is easier said then done because some providers do not support cross-domain and JSONP requests and all OAuth 1.0a request need to be signed with the consumer secret by the backend. Leave alone special request requirements invented by some zealous providers on top of the OAuth 1.0a and OAuth 2.0 standards.

The authomatic.access() function of the javascript.js library solves this for you. It encapsulates solutions of all the aforementioned issues and always makes the request in the most efficient way.

authomatic.access(loginResult.user.credentials, 'https://graph.facebook.com/{id}/feed',{
   substitute: loginResult.user, // replaces the {id} in the URL with loginResult.user.id.
   onAccessSuccess: function(data) {
      alert('Your most recent status is: ' + data.data[0].story);
   },
   onAccessComplete: function(textStatus) {
      if (textStatus == 'error') {
         alert('We were unable to get your Facebook feed!');
      }
   }
});
Fork me on GitHub