Maintainers¶
Authomatic was migrated from a private project of Peter Hudec to a community-managed project. Many thanks to Peter Hudec for all his hard work for creating and maintaining authomatic! We are now a small team of volunteers, not paid for the work here. Any help is appreciated!
Features¶
- Loosely coupled.
- Tiny but powerful interface.
- The python-openid library is the only optional dependency.
- Framework agnostic thanks to adapters. Out of the box support for Django, Flask, Pyramid and Webapp2.
- Ready to accommodate future authorization/authentication protocols.
- Makes provider API callls a breeze.
- 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¶
The package is licensed under MIT license.
Requirements¶
Requires Python 2.7 and newer. Python 3.x support added in Authomatic 0.0.11 thanks to Emmanuel Leblond <https://github.com/touilleMan>`__.
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¶
Contributions of any kind are very welcome.
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/authomatic/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:
- Make an instance of the
Authomatic
class. - 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 available up to version 0.1.0, but you will receive 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.
# -*- coding: utf-8 -*-
# main.py
import webapp2
from authomatic import Authomatic
from authomatic.adapters import Webapp2Adapter
from config import CONFIG
# Instantiate Authomatic.
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"
:
# -*- coding: utf-8 -*-
# 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 receive 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.
# Create a simple request handler for the login procedure.
def any(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.
result = authomatic.login(Webapp2Adapter(self), provider_name)
# Do not write anything to the response if there is no result!
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 there is result, the login procedure is over and we can write
self.response.write('<a href="..">Home</a>')
if result.error:
elif result.user:
# Hooray, we have the user!
# We need to update the user to get more info.
if not (result.user.name and result.user.id):
result.user.update()
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.
self.response.write(
# Seems like we're done, but there's more we can do...
if result.user.credentials:
self.response.write(
The call returns a Response
object. The Response.data
contains the parsed
response content.
url = 'https://graph.facebook.com/{}?fields=feed.limit(5)'
url = url.format(result.user.id)
# Access user's protected resource.
response = result.provider.access(url)
if response.status == 200:
# Parse response.
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 />')
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
.
Authomatic.access
AuthorizationProvider.access()
User.update()
Credentials.refresh()
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.
Authomatic.async_access
AuthorizationProvider.async_access()
User.async_update()
Credentials.async_refresh()
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, 'https://api.example.com/foo')
bar_future = authomatic.access(user.credentials, 'https://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.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.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¶
Popup¶
The Authomatic.login
function redirects the user to the provider
to ask him for his/her consent. If you rather want to make the redirect in a popup,
the authomatic.popupInit() function of the
javascript.js library with conjunction with LoginResult.popup_html()
make it a breeze.
Just add the authomatic
class to your login handler links and forms
and change their default behavior by calling the authomatic.popupInit() function.
The elements will now open a 600 x 800 centered popup on click or submit respectively.
you can change the popup dimensions with the authomatic.setup().
Set the onLoginComplete
event handler to the authomatic.setup() function,
which should accept a result
argument.
<!DOCTYPE html>
<html>
<head>
<title>Login Popup Example<title>
<!-- authomatic.js is dependent on jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="authomatic.js"></script>
</head>
<body>
<!-- Opens a popup with location = "login/facebook" -->
<a class="authomatic" href="login/facebook">Login with Facebook</a>
<!-- Opens a popup with location = "login/openid?id=me.yahoo.com" -->
<form class="authomatic" action="login/openid" method="GET">
<input type="text" name="id" value="me.yahoo.com" />
<input type="submit" value="Login with OpenID" />
</form>
<script type="text/javascript">
// Set up the library
authomatic.setup({
popupWidth: 600,
popupHeight: 400,
onLoginComplete: function(result) {
// Handle the login result when the popup closes.
if (result.user) {
alert('Hi ' + result.user.name);
} else if (result.error) {
alert('Error occurred: ' + result.error.message);
}
}
});
// Change behavior of links and form of class="authomatic"
authomatic.popupInit();
</script>
</body>
</html>
In your login handler just write the return value of the LoginResult.popup_html()
method
to the response.
import webapp2
import authomatic
from authomatic.adapters import Webapp2Adapter
class Login(webapp2.RequestHandler):
def any(self, provider_name):
result = authomatic.login(Webapp2Adapter(self), provider_name)
if result:
if result.user:
result.user.update()
self.response.write(result.popup_html())
The LoginResult.popup_html()
generates a HTML that closes the popup when the login procedure
is over and triggers the onLoginComplete
event handler with a JSON login result object passed as argument.
The login result object has similar structure as the LoginResult
.
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!');
}
}
});