Facebook Connect Authentication injected into ASP.NET MVC 3 Forms Authentication

I was recently admiring the simplicity of the PHP example on how to implement Server-side Authentication using OAuth 2.0 on facebook.com. You can read the full document here: [http://developers.facebook.com/docs/authentication/](http://developers.facebook.com/docs/authentication/)

The code below assumes that you’ve read the entire “Server-side Flow” section and have made some attempt to understand it.

I used Visual Studio 2010 and generated a new project of type “ASP.NET MVC 3 Web Application”, on the next step I choose Internet Application (once you’ve mastered the code below you’ll also be able to do this starting with Empty).

Next you can do some cleanup:

Delete Controllers/AccountController.cs

Delete Views/Account

In web.config you can delete the sections <membership>, <profile> and<roleManager>. You can also delete the <connectionStrings> section (or at least the one with the name “ApplicationServices”).

The reason for all this is that we won’t be using any of the built-in profile/membership/roles providers or the database connection string they rely upon.

Update the <authentication> node to look like this:

```xml
<authentication mode="Forms">

<forms loginUrl="~/Facebook/Login" slidingExpiration="false" />
</authentication>
```

Create a fake domain in your C:\Windows\System32\drivers\etc\hosts file (make sure you use this domain when you setup your Facebook application). For the sake of discussion I am going to use dev.somefakedomain.com. All you have to do is add this line to the file (you can edit it via Notepad - make sure you launch Notepad with Administrator privileges):

127.0.0.1 dev.somefakedomain.com

Modify your project settings to look like this image:

[![project settings](/images/content/b34b4457-19f6-4ef0-8b17-b1a5067af03f.png)](/images/content/b34b4457-19f6-4ef0-8b17-b1a5067af03f.png)

The port can be anything you like, as long as it's not used by another application on your machine (I use whatever port Visual Studio assigned).

Add a new controller. I called it Facebook (if you don’t call it "Facebook", don’t forget to change the loginUrl part of the <forms> tag in web.config).

Replace the entire body of your controller with this code:

```csharp
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Security.Cryptography;
using System.Net;
using System.IO;
using System.Web.Script.Serialization;
using System.Web.Security;

namespace FacebookLogin.Controllers
{
public class FacebookController : Controller
{
// these really should be in a config file:
private string appId = "getyourown!";
private string appSecret = "lookup your own!";
/*
* This is important. Using the default 'localhost' for debugging will fail,
* because of problems with writing cookies and because Facebook requires a real
* domain when you setup your application. What you have to do is create a 'fake'
* domain by editing your C:\Windows\System32\drivers\etc\hosts file.
* Google how to do this */
private string authorizeUrl = "http://dev.somefakedomain.com:2064/Facebook/Login";

public ActionResult Login()
{
try
{
string error = Request["error"];
if (!String.IsNullOrEmpty(error))
{
ViewBag.ErrorMessage = Request["error_description"];
}
else
{
string code = Request["code"];
// when we get redirected here by Forms Authentication we'll
// have a ReturnUrl indicating the page we came from
string returnUrl = Request.QueryString["ReturnUrl"];

if (String.IsNullOrEmpty(code))
{
// CSRF protection
var hashBytes = new MD5CryptoServiceProvider().ComputeHash(Guid.NewGuid().ToByteArray());
string state = Convert.ToBase64String(hashBytes);
Session["state"] = state;

// add the return Url to the state parameter so Facebook can send it all back to us
if (!String.IsNullOrEmpty(returnUrl))
{
state += returnUrl;
}

// good programmers encode strings before passing them to a query string
state = Url.Encode(state);

// don't forget to change the "scope" values listed below
// to something appropriate for your facebook application
// facebook warns not to get greedy as users may deny access
string redirectUrl = "http://www.facebook.com/dialog/oauth?client_id=" + appId +
"&redirect_uri=" +
Url.Encode(authorizeUrl) +
"&scope=publish_stream&state=" +
state;
return Redirect(redirectUrl);
}

string sessionState = Convert.ToString(Session["state"]);
string requestState = Request.QueryString["state"];

if (!String.IsNullOrEmpty(requestState) &&
!String.IsNullOrEmpty(sessionState) &&
requestState.Length >= sessionState.Length &&
requestState.Substring(0, sessionState.Length) == sessionState)
{
string tokenUrl = "https://graph.facebook.com/oauth/access_token?client_id=" + appId +
"&redirect_uri=" + Url.Encode(authorizeUrl) +
"&client_secret=" + appSecret +
"&code=" + code;
string response = GetPageContent(tokenUrl);

var responseDictionary = ParseQueryString(response);
if (responseDictionary.ContainsKey("access_token"))
{
// note: you don't HAVE to respect this, as long as you
// get a new token before trying to use the FB API
double facebookTokenExpirationSeconds =
Convert.ToDouble(responseDictionary["expires"]);

// Note: you may want to store responseDictionary["access_token"] and
// facebookTokenExpirationSeconds somewhere so you can use it for other FB API requests
string graphUrl = "https://graph.facebook.com/me?access_token=" +
responseDictionary["access_token"];

var serializer = new JavaScriptSerializer();
dynamic facebookUser = serializer.DeserializeObject(GetPageContent(graphUrl));

// grab facebook name and Id to use as our forms authentication ticket
string facebookName = facebookUser["name"];
long facebookUserId = Convert.ToInt64(facebookUser["id"]);

// get the cookie the way forms authentication would put it together.
//Note: I am using the facebookUserId as the "username" as it's guaranteed to be unique
var authCookie = FormsAuthentication.GetAuthCookie(facebookUserId.ToString(), true);
var ticket = FormsAuthentication.Decrypt(authCookie.Value);
// we want to change the expiration of our forms authentication cookie
// to match the token expiration date, but you can also use your own expiration
DateTime expiration = ticket.IssueDate.AddSeconds(facebookTokenExpirationSeconds);
var newTicket = new FormsAuthenticationTicket(ticket.Version, ticket.Name,
ticket.IssueDate, expiration, ticket.IsPersistent, facebookName);

// encrypt the cookie again
authCookie.Value = FormsAuthentication.Encrypt(newTicket);

// manually set it (instead of calling FormsAuthentication.SetAuthCookie)
Response.Cookies.Add(authCookie);

// If we added the Redirect Url to our 'state', grab it and redirect
if (requestState.Length > sessionState.Length)
{
returnUrl = requestState.Substring(sessionState.Length);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
}

// otherwise redirect back to Home or whatever your default page is
return RedirectToAction("Index", "Home");
}
else
{
ViewBag.ErrorMessage = "Facebook authorization replied with this invalid response: " +
response;
}

}
else
{
ViewBag.ErrorMessage =
"There is a problem with the redirect from Facebook. You may be a victim of CSRF.";
}

}
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "Login failed with this exception: " + ex.Message;
}

return View();
}

public ActionResult Logout()
{
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}

private static string GetPageContent(string url)
{
var request = WebRequest.Create(url);
var reader = new StreamReader(request.GetResponse().GetResponseStream());
return reader.ReadToEnd();
}

private static IDictionary ParseQueryString(string query)
{
var result = new Dictionary();

// if string is null, empty or whitespace
if (string.IsNullOrEmpty(query) || query.Trim().Length == 0)
{
return result;
}

foreach (var pair in query.Split("&".ToCharArray()))
{
if (!String.IsNullOrEmpty(pair))
{
var pairParts = pair.Split("=".ToCharArray());
if (pairParts.Length == 2)
{
result.Add(pairParts[0], pairParts[1]);
}
}
}

return result;
}
}
}
```

Add a Login view and replace it with code:

```csharp
@{
ViewBag.Title = "Facebook Login Error";
}

@if (!String.IsNullOrEmpty(ViewBag.ErrorMessage))
{

Error: @ViewBag.ErrorMessage

}
else
{

Facebook login encountered an error without a description.

}
```

Replace the contents of _LogOnPartial.cshtml with this code:

```csharp
@if (Request.IsAuthenticated)
{
string facebookName = "BLANK";
FormsIdentity ident = User.Identity as FormsIdentity;

if (ident != null)
{
FormsAuthenticationTicket ticket = ident.Ticket;
facebookName = ticket.UserData;
}

Welcome @facebookName!
[ @Html.ActionLink("Logout", "Logout", "Facebook") ]
}
else
{
@:[ @Html.ActionLink("Login", "Login", "Facebook") ]
}
```

Now that you’ve done this you can use the built-in [Authorize] tag on any controller method and the user will get sent to your Facebook/Login controller. The sky is the limit from here.

You could also use the Facebook SDK on codeplex, which provides a richer programming model and more functionality, but if you want to understand what's happening behing the scenes, this gives you a good idea of the handshake required for successful Facebook authentication.

Note that this example uses the built-in Session object, which will fail on a web server farm. You have a few options:

1) You can change the code that uses Session to rely on cookies instead

2) You can use a session provider that relies on a database or a separate app server

3) You can get rid of that code if you don't care about [CSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery)

If there is interest, I will post a working project example.