all that jazz

james' blog about scala and all that jazz

Facebook OpenID integration in Pebble

I've taken a shot at implementing Facebook OpenID integration into Pebble. To see the results, at the bottom of this page, click "Add Comment", and then click the Facebook Login link.

Implementing this has been pretty simple. All the magic happens on the client side, at no point does Pebble make a request on Facebook, and the user is, as far as Pebble is concerned on the server side, never authenticated or trusted in any way. All this integration does is puts the users name, profile picture and profile link in the author, avatar and website fields of the Pebble comment form.

The bulk of the work has been in modifying pebble to handle profile pictures with comments, and adding a plugin point for this type of plugin. What follows is a quick tutorial of how to implement a similar OpenID authentication on your site.

1. Create an application on Facebook

Facebook provides some simple instructions on how to do this here. You will need to create a new application for every different website that you want to integrate with, as Facebook will validate the base URL that it forwards to.

2. Add the login button to your page

There are a number of ways to do this, but the simplest way is to use the Facebook Markup Language (FML). In the next step we'll configure Facebook to render FML tags, for now, the only thing you need to do is put the following code where you want the login button to appear:

<fb:login-button/>

3. Initialise the Facebook API

The Facebook client side libraries require that they be initialised. This is also the stage where Facebook will render any FML tags you've included in the page. Initialising the Facebook client side API is best done at the bottom of the page, so that it doesn't slow down the loading of the rest of the page. At this stage you'll need the Facebook application ID from the application you created in step one:

<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
    FB.init({appId: "YOUAPPIDHERE", status: false, cookie: true, xfbml: true});
</script>

4. Write a function to handle a logged in user

This is where we do most of the work. The Facebook javascript API provides an event library, and we can use this to update our page with the users details after they log in to Facebook. This event will only fire when the user logs in or logs out, if a user comes back to your site, and they are already logged in, the event won't fire, even if they click log in, hence we also need to check if the user is already logged in when the page loads. So we're going to write a function that will handle both of these situations. The function accepts a Facebook response object, and checks if the response has a session. This indicates that the user is logged in. If they are logged in, we make a call on the /me resource of the Facebook Graph API, which will return the users name, link to their profile, and other things. This function can go anywhere in your code, preferably in a Javascript file along with the rest of your scripts:

  function updateFacebookCommentAuthor(response) {
    if (response.session) {
      FB.api("/me", function(response) {
        var avatar = "http://graph.facebook.com/" + response.id + "/picture?type=square";
        document.forms["commentForm"].elements["author"].value = response.name;
        document.forms["commentForm"].elements["website"].value = response.link;
        document.forms["commentForm"].elements["avatar"].value = avatar;
      });
    }
  }

5. Subscribe to login events

Lastly, we want the above function to be called when a page loads if the user is already logged in, and if a user logs in clicking the login function. This can either be done at the bottom of the page after the Facebook API is initialised, or it can be done in in the window on load event. Pebble uses prototype, so I bind it to the window on load event using that:

  Event.observe(window, "load", function() {
    FB.Event.subscribe("auth.sessionChange", updateFacebookCommentAuthor);
    FB.getLoginStatus(updateFacebookCommentAuthor);
  });

This is all you need for basic OpenID authentication using Facebook. Of course, you might want to, instead of just populating the form, actually render the username and profile picture in the page, as has been done in Pebble. Another thing you may want to provide is the ability for the user to log out, in case they are using someone else's computer and that person is logged in.

Facebook authentication in Java

If you're a web developer who likes writing practical, quick, and simple utility applications for yourself and others to use, then Facebook is the dream platform. You don't have to write any user management, sign in, password change pages etc, it comes complete with advanced user management, including friends lists. There's a high chance that your target users already use it, which makes them more likely to adopt your application because they don't need to go and create yet another username/password on yet another website. It already has a theme and style sheets, and will present your application surrounded in menus and title bars. And it provides a large number of ways to publish content and send notifications when events occur. All these features mean that you, the developer, can spend more time doing what you wanted to do, that is, writing your application.

If Java is your chosen platform however, you may encounter some difficulties. Java developers tend to like following well established design patterns. We like to think of things as beans that have distinct purposes. We like to use frameworks such as Spring to manage the beans that contain logic, to glue our application together. We like our data to be stored in beans, and we access that data using getters and setters. The Facebook client API makes this difficult, for the following reasons:

  1. Its instantiation is dependent on parameters in an HTTP request. This means we can't use it as a typical Spring bean.
  2. Depending on which flavour you of client you use, it returns JSON objects, or DOM documents, not the Java beans we like to use.

This article will address the first issue, and in doing so make the second issue less of a worry.

Instantiation of the client

Our first task is to gracefully handle the instantiation of the Facebook client. This will usually be required by most requests, hence, it is an ideal opportunity to use a ServletFilter. The guys at TheLiveWeb have already provided a very good article on how to do this, so I won't go into too much detail. The servlet filter basically intercepts every request, checks to see if there is already a client in the session, and if not, uses the fb_session parameter if it's an embedded fbml app, or auth_token parameter if it's not an fbml app, to instantiate the client. If neither of those parameters exist, it redirects the user to the login page. The ServletFilter I've written is very different to theirs, but both follow the same concept and achieve the same goal.

Using a ThreadLocal to access the client

Our next task is to make the client available to the backend service beans that have no knowledge of the HTTP session. We do this using a ThreadLocal. We start by creating a wrapper class, UserClientBean, to wrap all calls to the client. The client itself is stored in a private static ThreadLocal field:

public class UserClientBean
{
    public static void setClient(FacebookXmlRestClient aClient)
    {
        client.set(aClient);
    }
    public static void remove()
    {
        client.remove();
    }
    private static final ThreadLocal client
        = new ThreadLocal();
    ...
}

So calling UserClientBean.set() will bind a facebook client to the current thread, and UserClientBean.remove() will remove it. We make these calls from the servlet filter:

public void doFilter(ServletRequest aRequest, ServletResponse aResponse,
        FilterChain aChain) throws IOException, ServletException
{
    if (aRequest instanceof HttpServletRequest)
    {
        HttpServletRequest request = (HttpServletRequest) aRequest;
        HttpServletResponse response = (HttpServletResponse) aResponse;
        HttpSession session = request.getSession();
        FacebookXmlRestClient client = (FacebookXmlRestClient) session
                    .getAttribute("facebookClient");
        if (client == null)
        {
            String sessionKey = request
                    .getParameter(FacebookParam.SESSION_KEY.toString());
            String authToken = request.getParameter("auth_token");
            if (sessionKey == null && authToken == null)
            {
                response.sendRedirect(loginPage);
                return;
            }
            else
            {
                try
                {
                    if (sessionKey != null)
                    {
                        client = new FacebookXmlRestClient(apiKey, secret,
                                sessionKey);
                    }
                    else
                    {
                        client = new FacebookXmlRestClient(apiKey, secret);
                        client.auth_getSession(authToken);
                    }
                    client.setIsDesktop(false);
                    session.setAttribute("facebookClient", client);
                }
                catch (FacebookException fe)
                {
                    log.error("Unable to log into facebook", fe);
                }
            }
        }
        // Store the client in the thread
        UserClientBean.setClient(client);
    }
    aChain.doFilter(aRequest, aResponse);
    // Remove the client from the thread
    UserClientBean.remove();
}

If you've never come across ThreadLocal's before, a ThreadLocal is a class that binds the object passed to its set() method to the current thread. Calling the get() method will retrieve that object from the current thread, if it exists. ThreadLocal's are usually declared as static final, so that they can be accessed statically from anywhere in the application.

Using the client

Now that we can access the client as a service, we can add calls as desired to UserClientBean to run on the FaceBookRestClient, and extract the data out in a way that's nice to return to our application. For example:

public List getAppFriends()
{
    List result = new ArrayList();
    try
    {
        Document d = client.get().friends_getAppUsers();
        Node n = d.getFirstChild();
        while (n != null)
        {
            result.add(Long.parseLong(n.getTextContent()));
            n = n.getNextSibling();
        }
    }
    catch (Exception e)
    {
        log.error("Error retrieving logged in facebook user", e);
    }
    return result;
}

public boolean isFriend(long aFriendId)
{
    try
    {
        FacebookXmlRestClient c = client.get();
        long user = c.users_getLoggedInUser();
        if (user == aFriendId)
        {
            return true;
        }
        else
        {
            Document d = c.friends_areFriends((int) user, (int) aFriendId);
            Node n = d.getElementsByTagName("are_friends").item(0);
            if (n.getTextContent().equals("1"))
            {
                return true;
            }
        }
        return false;
    }
    catch (Exception e)
    {
        log.error("Error determining if users are friends", e);
        return false;
    }
}

As you may have noticed, the FaceBookRestClient uses ints, while Facebook recommends user ids being stored as longs. Not very smart huh? At least using this client, our apps will be ready for that bug fix.

About

Hi! My name is James Roper, and I am a software developer with a particular interest in open source development and trying new things. I program in Scala, Java, Go, PHP, Python and Javascript, and I work for Lightbend as the architect of Kalix. I also have a full life outside the world of IT, enjoy playing a variety of musical instruments and sports, and currently I live in Canberra.