OAuth 2.0
Recently I've been working on a simple Facebook app called SyncitySync, which updates your Facebook status with your tweets. One of the fiddlier bits of work involved authorising the app with Facebook in order to make offline updates to the user's status. In theory OAuth 2.0 is straightforward, but Facebook adds its own wrinkles, particularly when using an iframe.
- The client (eg our app) redirects the user to an authorisation server.
- The user authenticates themselves with the authorisation server.
- The authorisation server sends back a verification code.
- The client requests an access token using the verification code and its own credentials.
- The authorisation server sends back an access token, assuming all is well.
- The access token can then be used by the client.
There's a
helpful page in the Facebook Developer Docs which describes this in terms of a typical Facebook app. If you haven't already read this, please read it first! Here are some implementation notes gathered while doing this in practice. As discussed before, all this is written in Java, using Spring MVC with annotations, hosted on Google App Engine.
Configuring Facebook
I'm going to assume you have already created an app record in Facebook, complete with client id and secret etc. Now we need to configure the settings for our app in Facebook. We set the following settings under "Facebook Integration":
We also enable OAuth 2.0 under "Advanced":
Pre-Authorised Users
The first thing to do is to check that the user has already authorised your app. This is quite straightforward to do in a controller:
final String encryptedSignedRequest = request.getParameter("signed_request");
if (!StringUtils.isEmpty(encryptedSignedRequest)) {
final String decryptedSignedRequest = FacebookUtil.validateSignature(encryptedSignedRequest,
facebookConfiguration.getClientSecret());
Gson gson = new Gson();
final LoggedInUser user = gson.fromJson(decryptedSignedRequest, LoggedInUser.class);
if (user.getUser_id() != null && user.getUser_id() != 0L) {
request.getSession().setAttribute(SessionAttributes.LOGGED_IN_USER, user);
return "redirect:register.do";
}
}
return "facebookLogin";
Here I extract the
signed_request parameter, and decrypt it (see below) into a JSON object called LoggedInUser using
Gson. LoggedInUser is simply a representation of the signed_request as per the
Canvas OAuth docs. In particular, we check for the existence of the user_id property. This will be null unless the user has already authorised the app.
If the user is already authorised, we redirect to the registration form (register.do). Otherwise we redirect to the authorisation server in order to get the verification code. Well, that would be the case if you weren't using an iframe.
FacebookUtil.validateSignature is a simple library method that validates a signed_request and decrypts it if valid.
String[] parts = signed_request.split("\\.");
String encSig = parts[0];
String encPayload = parts[1];
Base64 decoder = new Base64(true);
String data = new String(decoder.decode(encPayload));
try {
Mac mac = Mac.getInstance("HMACSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(), mac.getAlgorithm()));
byte[] calcSig = mac.doFinal(encPayload.getBytes());
if (Arrays.equals(decoder.decode(encSig), calcSig)) {
return data;
} else {
return null;
}
} catch (InvalidKeyException e) {
throw new Exception("Failed to perform crypt operation.", e);
}
In theory one can redirect the user immediately to
https://graph.facebook.com/oauth/authorize at this point in the controller. However, for an iframe this will not work. You will end up with Facebook within Facebook (i.e. your iframe will contain the whole facebook surround plus your app, not just your app) and other weirdness. This caused some head scratching but I eventually found
this discussion.
What you have to do is redirect to a page containing some javascript which will then do the redirection - where it says "facebookLogin" in the controller code above, it takes us to a jsp which contains only this javascript:
A quick word about the parameters here. The scope declares that the permissions sought will be to update the user's stream, and to be able to do so after the user has logged out. We need these to be able to update the user's status in Facebook from Twitter while they are not logged into the former. Without the scope, the app's permissions will lapse after a short time.
The redirect_uri will be called upon successful authorisation, and is the subject of Part 2.