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/
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:
<authentication mode="Forms">
<!-- we don't want the expiration to slide, because we respect the Facebook token expiration settings -->
<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:

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:
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:
@{
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:
@if (Request.IsAuthenticated)
{
string facebookName = "BLANK";
FormsIdentity ident = User.Identity as FormsIdentity;
if (ident != null)
{
FormsAuthenticationTicket ticket = ident.Ticket;
facebookName = ticket.UserData;
}
Welcome <strong>@facebookName</strong>!
[ @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
If there is interest, I will post a working project example.