Magnus Rahl
Feb 15, 2011
  7468
(2 votes)

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: true
   9:         });
  10:         FB.Event.subscribe('auth.sessionChange', function (response) {
  11:             if (response.session) {
  12:                 // The user has logged in, reload to integrate user
  13:                 window.location = window.location;
  14:  
  15:             } else {
  16:                 // The user has logged out, and the cookie has been cleared
  17:             }
  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

Feb 15, 2011

Comments

Anders Hattestad
Anders Hattestad Feb 15, 2011 08:15 PM

nice!

Magnus Rahl
Magnus Rahl Feb 16, 2011 10:25 AM

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.

Please login to comment.
Latest blogs
Opti ID overview

Opti ID allows you to log in once and switch between Optimizely products using Okta, Entra ID, or a local account. You can also manage all your use...

K Khan | Jul 26, 2024

Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024