Posts tagged ‘ASP.NET’

March 20, 2012

Securing static content with ASP.NET forms authentication

A colleague of mine recently asked me how to secure static content on an IIS server and I thought I would quickly list the steps here for others that are looking for a quick guide on how to do it.

First, the URL Authorization role service should be enabled on the IIS server. To do this, open the Server Manager and go to Roles -> Web Server (IIS) -> Add Role Services and then click the checkbox for URL Authorization.

Next, the Manage pipeline mode should be set to Integrated for the Application Pool that the application is running under. You can verify this by opening the IIS Manager and going to the Connections pane. Expand ‘Sites’ and navigate to your web site (or application). In the Actions pane, click Advanced Settings. Then click on the General Section followed by clicking the Application Pool entry.

Lastly, the web.config will need to instruct IIS to use ASP.NET’s UrlAuthorization Module and / or FormsAuthentication module. Here’s an example for both:

  <system.webServer>
    <modules>
. . . removed other modules …
      <remove name="FormsAuthenticationModule" />
      <add name="FormsAuthenticationModule" type="System.Web.Security.FormsAuthenticationModule" />

      <remove name="UrlAuthorization" />
      <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />

    </modules>

Forms authentication requires that you specify <authorization> tags. Here’s an example that allows ‘anonymous’ to download images:

<configuration>
  <system.web>
    <authorization>

<deny users=”?” />

</authorization>

. . . removed other config . .

  </system.web>

  <location path="ErrorPages">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>

  <location path="Images">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>

This should be all that is required for securing your static content with ASP.NET forms authentication. For further reading, I would suggest this page.

Tags: ,
June 26, 2011

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/

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:

project_settings

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 @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

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

Follow

Get every new post delivered to your Inbox.