Creating a Custom Login Page for Windows Authentication

A month ago, I wrote an article talking about how to create a custom login page for FBA in SharePoint 2010. Since then, I got some enquiries asking whether it is possible to do the same thing for Windows Authentication. So I did some research and created another demo, and here comes this article. Again, I don’t know whether this is a supported way. So you’ll have to take your own risk when you use it in your production.

Preparation

In this demo, I am dealing with the following scenario. Say in my org every user has his/her own Personal Identity Number (PIN). When they login to the SharePoint site, in addition to the user credential of AD, I also want them to use their PIN as a double check. I don’t want them input anything except PIN. Here comes how I do it by customizing the login page of the Windows Authentication.

In a Claims-mode web app, the Windows Authentication is handled by /_windows/default.aspx. If we check the path of the virtual folder /_windows, it points to C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\IDENTITYMODEL\WINDOWS. Obviously, modifying the files in this folder directly is not a good idea. So what I did first is to create another folder, copy all files (default.aspx and web.config) in the above folder to the folder I created, and modify the path of the virtual folder to point to the new folder. I tested the site with the new folder. Everything works fine.

Creating a Customer Login Page

All the same as what I did for the login page for FBA, I created a page using simple.master. Since there is no need for users to input user name and password, a login control is not necessary. Instead, I just put a textbox, a button and several labels on the page. The layout looks like the following.

image

Creating a Code Behind Class to Handle the Request

I then created a class named as WindowsSignInPage and make it derive from IdentityModelSignInPageBase just like the forms sign in page. Again, I added all necessary assembly references.

Now it is time to write the logic to handle the real authentication. Different from the Forms authentication, in Windows authentication in Claims mode users are not authenticated by /_windows/default.aspx directly. Instead, they are authenticated by an HttpModule, SPWindowsClaimsAuthenticationHttpModule. When a user browse the web site and reaches /_windows/default.aspx, he/she has already passed the Windows authentication performed by that HttpModule. So to force users to input their PIN on the page, we must sign them out first. The following is the code I added in Page_Load.

 IClaimsIdentity claimsIdentity =
    HttpContext.Current.User.Identity as IClaimsIdentity;
if (claimsIdentity != null)
{
    CurrentUserLit.Text = claimsIdentity.GetDisplayName();
}
else
{
    CurrentUserLit.Text = HttpContext.Current.User.Identity.Name;
}

SecurityToken token = SPSecurityContext.SecurityTokenForContext(Context.Request.Url);
HttpCookie signInCookie = Context.Request.Cookies["Morpheus_WindowsSignedIn"];
if (signInCookie != null && signInCookie.Value.Equals("true"))
{
    RedirectToSuccessUrl();
}
else
{
    RemoveCookiesAndSignOut();
}

Since the user has passed the authentication of the HttpModule, the identity we get has been an IClaimsIdentity already. Here I check my custom cookie first. If the cookie exists, means the user has input the correct PIN. Otherwise, the user is signed out so he/she must input the PIN first.

The code of RemoveCookieAndSignOut is as below. If you examine the code of Signout.aspx with Reflector, you will see that I am using almost the same logic with it.

 private void RemoveCookiesAndSignOut()
{
    // Clear session state. 
    if (Context.Session != null)
    {
        Context.Session.Clear();
    }

    string cookieValue = string.Empty;
    if (Context.Request.Browser["supportsEmptyStringInCookieValue"] == "false")
        cookieValue = "NoCookie";

    // Clear my own cookie.
    HttpCookie cookieWinSignIn = Context.Request.Cookies["Morpheus_WindowsSignedIn"];
    if (cookieWinSignIn != null)
    {
        cookieWinSignIn.Value = cookieValue;
        Context.Response.Cookies.Remove("Morpheus_WindowsSignedIn");
        Context.Response.Cookies.Add(cookieWinSignIn);
    }

    // Remove cookies for authentication. 
    HttpCookie cookieSession = Context.Request.Cookies["WSS_KeepSessionAuthenticated"];
    if (cookieSession != null)
    {
        cookieSession.Value = cookieValue;
        Context.Response.Cookies.Remove("WSS_KeepSessionAuthenticated");
        Context.Response.Cookies.Add(cookieSession);
    }

    HttpCookie cookiePersist = Context.Request.Cookies["MSOWebPartPage_AnonymousAccessCookie"];
    if (cookiePersist != null)
    {
        cookiePersist.Value = cookieValue;
        cookiePersist.Expires = new DateTime(1970, 1, 1);
        Context.Response.Cookies.Remove("MSOWebPartPage_AnonymousAccessCookie");
        Context.Response.Cookies.Add(cookiePersist);
    }

    // Sign out.
    Microsoft.IdentityModel.Web.FederatedAuthentication.SessionAuthenticationModule.SignOut();
}

The final part is very simple. When the user clicks the submit button, I check his/her PIN first. If the PIN is correct, I set the cookie and redirect him/her to this page again so that the HttpModule authenticates the user again.

 protected void LoginBtn_Click(object sender, EventArgs e)
{
    // Check whether the PIN is correct.
    if (!string.Equals(pinCode.Text.ToLower(), pinLit.Text.ToLower()))
    {
        ClaimsFormsPageMessage.Text = "The PIN is incorrect.";
        return;
    }

    // Set cookie and redirect to the sign in page
    // so the SPWindowsClaimsAuthenticationHttpModule can sign us in again.
    HttpCookie cookie = Context.Request.Cookies["Morpheus_WindowsSignedIn"];
    if (cookie == null)
    {
        cookie = new HttpCookie("Morpheus_WindowsSignedIn");
    }
    else
    {
        Context.Response.Cookies.Remove("Morpheus_WindowsSignedIn");
    }
    cookie.Value = "true";
    cookie.Expires = DateTime.Now.AddMinutes(1); // 1 minute for testing purpose.
    Context.Response.Cookies.Add(cookie);
    SPUtility.Redirect("/_windows/default.aspx", SPRedirectFlags.Default, Context);
}

Deploying and Testing

To deploy the page, connect the default.aspx and the assembly by adding a <%@ Page %> indicator, put default.aspx in /_windows and the assembly in GAC, and reset the app pool. Then browse the site to see the result.

The attachment is the source code of the demo.

WindowsSignInPage.zip