This post is the second of three posts about advanced authentication in TurboGears 2. In Part 1, the first post, we learn how to manually configure authentication and authorization in a TurboGears project. The same results can be achieved using the quickstart command, however, configuring the authentication middleware manually give us more control and allow us to easily add support for other authentication methods.
Part 2 is about adding support for FacebookConnect which allow users to login to a website using their Facebook acounts. You should read and complete the 9 steps from Part 1 before start following the instructions below. A working TurboGears 2 project including all changes introduced in Part 1 can be downloaded from GitHub.
After you have downloaded the project from GitHub or completed the steps in Part 1, let’s continue the process by adding FacebookConnect support:
Step 10. Add Facebook Python SDK to the project
The purpose of having the user to login into our site is to be able to know something about that particular user. Normally we would like to know its name and email address, so whether the user is logging in with an username and password or using its Facebook account, we would like to have access to that kind of information; hopefully without having to ask the user for it.
Facebook users already have provided a name, an email address and a lot more information when they create an account. If the user has agreed to share it, Facebook allows applications to access that information through the Graph API and to use that API we’re going to need two things:
- A Facebook Application.
- A Library to consume the API resources.
Let’s start with the library: in order to retrieve profile information from Facebook we’re going to use Facebook Python SDK, the official library to interact with Facebook Graph API. It provides the necessary methods to parse an AccessToken from a cookie created by another Facebook library – Facebook JavaScript SDK – and to read and write data to Facebook.
Right now, all you need to do, is download http://github.com/facebook/python-sdk/blob/master/src/facebook.py to the same directory that contains your development.ini. We’ll use the library in the next step.
Step 11. Create a plugin to authenticate users with Facebook Connect
When an user is logged into his/her Facebook account and decides to login to our site using his/her Facebook credentials, the Facebook JavaScript SDK creates a special cookie under our domain. The information stored in that cookie allows us to gather information from Facebook through the Graph API.
In Step 11. we create a repoze.who plugin which look for Facebook credentials in the cookies of each request. If the credentials are found, the plugin attempts to relate those credentials with an existing user in our database, if such an user doesn’t exits the plugin try to read the user’s name and email address from Facebook and then creates a new user in the database. Whether the credentials could be related to an existing user or a new one was created, the plugin return an “identity” dictionary to tell repoze.who it has successfully authenticated a valid user.
There are four types of repoze.who plugins: Identifier plugins, Authenticator plugins, Metadata Provider plugins and Challenger plugins. FacebookConnectPlugin is both an Identifier plugin an a Metadata Provider plugin:
An Identifier plugin examines the WSGI environment and attempts to extract credentials from the environment. These credentials are used by authenticator plugins to perform authentication. In some cases, an identification plugin can “preauthenticate” an identity (and can thus act as an authenticator plugin).
Metadata provider plugins are responsible for adding arbitrary information to the identity dictionary for consumption by downstream applications. For instance, a metadata provider plugin may add “group” information to the the identity.
Identifier plugins should expose three methods:
- identify: called on every request, is expected to go trough the WSGI environment looking for credential information. In our case the credential information is expected to be in a cookie, but it could be a form field, a request header, a WSGI environment variable or anything else that can be used to store authentication information. If the plugin find credentials the method should return an “identity”: this must be a dictionary, normally with a login key with whatever our application is using to identify users as its value. If no credentials are found the method should return None.The “identity” returned by the identify method is then used by an Authenticator plugin to authenticate the user, however, an Identifier plugin can pre-authenticate an user if the credentials are good enough. In that case, the identify plugin should include the key repoze.who.userid in the “identity” dictionary.
- remember: when an user is authenticated in a request we would like he or she to remain authenticated for the rest of the session. The remember method is in charge of taking the necessary measures to make sure the given “identity” is remembered by the client and can be used in future requests so we don’t need to ask the user to login again. Normally a remember method return the necessary headers to, for example, create a cookie in the browser with the identity information.In our case, the creation or deletion of the cookie with the user credentials is handled by an external component: the Facebook JavaScript SDK. The cookie will exist as long as the user is logged into his/her Facebook account and thus we don’t need to worry about remembering any credentials. Our remember method does nothing.
- forget: user sessions shouldn’t last forever; the work of the forget method is to destroy or delete whatever is keeping the session alive when the user decides to logout or the session has been active for more than the allowed time. Normally the forget method just return headers to expire some cookies.As in with the remember method, we don’t need to worry about forgetting the credential information since that is a job for Facebook JavaScript SDK so this method also does nothing in our plugin.
Metadata Provider plugins should expose one method:
- add_metadata: this method arbitrarily add information to the identity dictionary based in other data in the environment or identity. Our plugin adds the access token to the identity. The token can later be used to read or write to Facebook additional information about the user.
Below is the code for the plugin described above. It should be added to the project.lib.auth module:
import transaction
from repoze.who.interfaces import IIdentifier, IMetadataProvider
from sqlalchemy.exc import UnboundExecutionError
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from webob import Request
from zope.interface import implements
import facebook
class FacebookConnectPlugin(object):
implements(IIdentifier, IMetadataProvider)
def __init__(self, appid, secret, **kw):
self.appid = appid
self.secret = secret
def identify(self, environ):
request = Request(environ)
params = facebook.get_user_from_cookie(request.cookies, self.appid, self.secret)
if params is None:
# user is not logged in to his/her facebook account or hasn't granted
# access to this application
return None
fb = facebook.GraphAPI(params['access_token'])
try:
user = DBSession.query(User).filter(User.fbid==params['uid']).one()
except (NoResultFound, MultipleResultsFound):
try:
profile = fb.get_object('me')
except facebook.GraphAPIError:
return None
if 'id' not in profile:
# we couldn't get any information from facebook. login failed.
return None
user = User(email=profile['email'], name=profile['name'], fbid=profile['id'])
DBSession.add(user)
transaction.commit()
try:
return {'repoze.who.userid': user.email, 'facebook': True}
except UnboundExecutionError:
return {'repoze.who.userid': profile['email'], 'facebook': True}
def remember(self, environ, identify):
# Facebook JavaScript SDK handles cookies creation and deletion
pass
def forget(self, environ, identity):
# Facebook JavaScript SDK handles cookies creation and deletion
pass
def add_metadata(self, environ, identity):
request = Request(environ)
user = identity.get('user', None)
params = facebook.get_user_from_cookie(request.cookies, self.appid, self.secret)
if params is not None and user is not None:
# facebook account is already linked to this user account
if user.fbid == params['uid']:
# give the application access to the OAuth token
identity['facebook.token'] = params['access_token']
# this user account is already linked to a different facebook account
elif user.fbid is not None:
# there is valid FacebookConnect session but the current user account is linked to a different Facebook account.
pass # TODO: ask the user if he/she wants to switch accounts
# this user account is not linked to a facebook account
else:
# there is valid FacebookConnect session but the current user account is not linked to it.
pass # TODO: ask the user if he/she wants to link this account
# TODO: what if the FacebookConnect session is linked to another user account?
The above add_metadata method is partially implemented; it only adds the token information to the identity dictionary and leaves some open problems about how to handle account collisions. For example, if an user is logged in our site using username and password and he just accessed his/her facebook account, the application could ask that user to link the two accounts.
Step 12. Update who.ini to use FacebookConnectPlugin
In order to use the plugin created in Step 12. we need to update the who.ini file we created in Part 1 (Step 5). Let’s do that by adding the following text before the [general] section of the configuration:
[plugin:facebook]
# FacebookConnect identification and authorization
use = project.lib.auth:FacebookConnectPlugin
appid = YourFacebookApplicationAppId
secret = YourFacebookApplicationSecret
Then add facebook to the plugins listed in identifiers and mdproviders sections. Those sections should now looks something like:
[identifiers]
# We can decide who the user is trying to identify as using either
# a fresh form-post, or the session identifier cookie
plugins =
form;browser
ticket
facebook
# ...
[mdproviders]
# Metadata providers are the things that actually look up a user's credentials
# here we have a plugin that provides "user" information (user) and another,
# which acts as an adapter to the first, to provide group/permission information.
plugins =
project.lib.auth:user
project.lib.auth:group
facebook
Step 13. Update the templates
The last step in adding support for FacebookConnect to our TurboGears2 project is to update the templates with the Facebook JavaScript SDK code. Open project/template/master.html and add the following code at the bottom of the <body> tag (Don’t forget to provide the App ID of your own Facebook application in the appId parameter of the FB.init() function):
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
FB.init({appId: YourFacebookApplicationAppId, status: true, cookie: true, xfbml: true});
FB.Event.subscribe('auth.sessionChange', function(response) {
if (response.session) {
// A user has logged in, and a new cookie has been saved
window.location.reload()
} else {
// The user has logged out, and the cookie has been cleared
window.location.reload()
}
});
</script>
As explained at the beginning of this post, the previous snippet uses Facebook JavaScript SDK to create a cookie in our domain with enough information to fetch information from Facebook servers on behalf of the logged in user. When a users logs in or log out from Facebook, his/her session status change and the snippet above will reload the page so the TurboGears authentication and authorization middleware can take the necessary actions using the plugin we just created.
Finally, we need a way to ask the user to give us permission to access his data on facebook; for that, we are going to add the code to show a Facebook login button in the login page.
Open project/templates/login.html and add the following:
<fb:login-button perms="email"></fb:login-button>
Conclusion
Adding support for Facebook Connect was easy compared to what we had to do Part 1 of this post. However, is because how well we prepared TurboGears2 authentication and authorization middleware that now we are able to add support for other authentication methods without much effort.
In Part 3 we’ll see how to add support for Sign in with Twitter and I’m sure it will be as easy as with Facebook Connect.
Let me know what you think and if you find any bugs.
The code for the example TurboGears2 project, including steps 1-13 is available at GitHub and soon will be updated with steps from the next and last post.