Single Sign-on using Facebook javascript library, Part 1
The RelatePlus templates come with a ready-to-use Open ID integration, but I find users are more likely to have a Facebook account which they use to log in to various web applications outside of Facebook. Facebook offers several different integration points depending on how deep integration you are looking for. The most simple cases just use javascript and ready-to-use “controls” to enable the infamous “like” button and its cousins.
In this article I describe how you can use the relatively simple javascript approach together with an http module to enable users to sign in to your site with their facebook account, while still creating a asp.net membership account and logging the user in locally in to your site.
Step 1: The client javascript and login button
This isn’t specific to neither EPiServer nor ASP.NET so I won’t go over the details. There is some documentation over at the Facebook API documentation pages. To quckly sum it up:
First I generated an application ID in Facebook. You must have this to use the Facebook API. Then I created a User Control containing the client script and stuck this control in the master page, first inside the body tag. So it loads on every page. The markup is pretty much copy-and paste from the Facebook API docs:
1: <div id="fb-root"></div>
2: <script type='text/javascript'>1:
2: window.fbAsyncInit = function () {3: FB.init(
4: {
5: appId: 'APP_ID_GOES_HERE',6: status: true,7: cookie: true,8: xfbml: true9: });
10: FB.Event.subscribe('auth.sessionChange', function (response) {11: if (response.session) {12: // The user has logged in, reload to integrate user13: window.location = window.location;
14:
15: } else {16: // The user has logged out, and the cookie has been cleared17: }
18: });
19: };
20: (function () {21: var divId = 'fb-root';22: var e = document.createElement('script'); e.async = true;23: e.src = document.location.protocol +
24: '//connect.facebook.net/sv_SE/all.js';25: document.getElementById(divId).appendChild(e);
26: } ());
</script>
Finally I used XFBML to add a Facebook login button on my login page, complete with the requested extended permission to read the user’s email:
1: <fb:login-button perms="email" />
Simple as that. The users can now click the login button, grant my application access to their basic account details and email, and we’re go for the client side. Using the javascript API I can now call Facebook services using the user’s access token to pull or push Facebook data and to load Facebook controls. But this far, the EPiServer site has no clue about the user.
Step 2: Integrate the user using a http module
By creating an http module which run on every (managed) request, I can use the great Facebook C# SDK to pick up the Facebook session whenever a user is logged in using the client script. Using the session data I check if there is a local membership user corresponding to the logged in Facebook user. If not I create one using the Facebook user ID as name. For future extendability I also save the Facebook user ID in the profile, so I add a field for this to my web.config profile section:
1: <profile enabled="true" defaultProvider="SqlProfile" automaticSaveEnabled="true">
2: <properties>
3: ...
4: <add name="FacebookId" type="System.Int64"/>
5: </properties>
6: ...
7: </profile>
I also save the user’s email address (requested in the extended permission), first and last name. Finally, I set the forms auth cookie to log the user in and refresh the page for the login to take effect. Here’s the code for the http module:
1: using System;
2: using System.Web;
3: using System.Web.Security;
4: using System.Web.Profile;
5: using EPiServer.Personalization;
6:
7: namespace ACME.Web.Templates.HttpModules
8: {
9: public class FacebookIntegrationModule : IHttpModule
10: {
11: public void Dispose() { }
12:
13: public void Init(HttpApplication context)
14: {
15: context.AuthenticateRequest += AuthenticateRequest;
16: }
17:
18: private void AuthenticateRequest(object sender, EventArgs e)
19: {
20: var application = sender as HttpApplication;
21:
22: if (application == null || application.Request.IsAuthenticated) return;
23:
24: var facebookApp = new Facebook.FacebookApp();// Construct fbsession from cookie
25: UpdateUser(application, facebookApp;
26: }
27:
28: private bool UpdateUser(HttpApplication application, Facebook.FacebookApp facebookApp)
29: {
30: if (facebookApp.Session == null || facebookApp.Session.UserId < 1) return false;
31:
32: // Read user data
33: var userId = facebookApp.Session.UserId;
34: var username = userId.ToString();
35: dynamic me = facebookApp.Get("me");
36: string email = me.email;
37: string firstname = me.first_name;
38: string lastname = me.last_name;
39:
40: // Get user, create if not previously exiting
41: var user = Membership.GetUser(username);
42: if (user == null)
43: {
44: // Password won't be used with FB login, so just set a new random one
45: var password = Membership.GeneratePassword(10, 0);
46: user = Membership.CreateUser(username, password, email ?? username + "@acme.web");
47: }
48:
49: var profile = EPiServerProfile.Get(username);
50:
51: // Make user the ID is set in a profile property
52: var savedId = GetFacebookId(profile);
53: if (savedId != facebookApp.Session.UserId)
54: {
55: SetFacebookId(profile, userId);
56: }
57:
58: if (email != null)
59: {
60: // UPdate email both in membership and profile
61: if (user.Email != email)
62: {
63: user.Email = email;
64: Membership.UpdateUser(user);
65: }
66:
67: if (profile.Email != email)
68: {
69: profile.Email = email;
70: }
71: }
72:
73: if (profile.FirstName != firstname)
74: {
75: profile.FirstName = firstname;
76: }
77:
78: if (profile.LastName != lastname)
79: {
80: profile.LastName = lastname;
81: }
82:
83: profile.Save();
84:
85: FormsAuthentication.SetAuthCookie(user.UserName, false);
86:
87: return true;
88: }
89:
90: private static long GetFacebookId(ProfileBase profile)
91: {
92: var value = profile["FacebookId"];
93: return Convert.ToInt64(value);
94: }
95:
96: private static void SetFacebookId(ProfileBase profile, long id)
97: {
98: profile["FacebookId"] = id;
99: }
100: }
101: }
As you can see this is C# 4.0 code using dynamics, but it is possible to use the Facebook library the same way (but with different syntax) in .NET 3.5. The http module must then be added to the system.webServer handlers element in web.config:
1: <system.webServer>
2: <modules runAllManagedModulesForAllRequests="true">
3: ...
4: <add name="FacebookIntegrationModule" type="ACME.Web.Templates.HttpModules.FacebookIntegrationModule, ACME.Web" preCondition="managedHandler"/>
5: </modules>
6: ...
7: </system.webServer>
Once a user has granted your application access, the user won’t be prompted again unless you change the requested permissions. If the user is already logged in to Facebook she won’t even have to sign in, the login will be automatic as soon as your page is visited!
Step 3: Fix the bugs :)
Make sure you test your solution with the browsers you are targeting. I also advice you to think about the javascript refreshing of the site when the session becomes available. As I will describe in a separate article I had problems with one specific Safari browser which got stuck in an infinite loop because it couldn’t persist the session and therefore continued to fire the sessionChanged event.
Source code
The code in this example is manually edited to enable a quick overview in few files. I copied and pasted code from several classes so it is likely to contain syntax errors etc. These edited files are available for download in the code section
nice!
Ah, I spotted the first cut-and-paste error :) The actual call to UpdateUser() is missing in the code for the http module. Without that it won't do much :) I'll try to update the post.