Creating a Custom Login Page for FBA in SharePoint 2010

The default login page of FBA in SharePoint 2010 is very simple. It only provides a Login control with the user name, password and remember me option. We usually have to create a custom login page to accommodate other security requirements, for example, adding a verification/secure code option on the page, other performing a double secure check etc.

As FBA in SharePoint 2010 has been changed to be based on Claims authentication, creating a custom login page for it is a quite different from MOSS 2007. As Steve Peschka mentioned, the FormsAuthentication class is not used anymore. Now we need to deal with the SharePoint STS and Claims when authenticating users. Fortunately, SharePoint 2010 provides us interfaces to make the things simple. In this article, I am going to show you how we can create a custom login page with the build-in master page and look-and-feel.

Please note that I am not sure whether the way I mentioned here is official supported by Microsoft Support. So you’ll have to take your own risk when you use it in your production.

Creating a Custom Login Page with the Build-in Look-and-Feel

If we take a look at the login page of a FBA enabled site, we will find that it is actually the default page in the virtual folder _forms. The design in this way just make things easier because every web app has its own login page. The modification on one web app will not affect others.

To add a Secure code option on the page, I add the following code in the Login control.

  <asp:login id="loginControl" 
    FailureText="<%$Resources:wss,login_pageFailureText%>" 
    runat="server" width="100%" OnLoggingIn="signInControl_LoggingIn" 
    OnAuthenticate="signInControl_Authenticate">
    <layouttemplate>
        <asp:label id="FailureText" class="ms-error" runat="server"/>
        <table width="100%">
        <tr>
            <td nowrap="nowrap">
                <SharePoint:EncodedLiteral runat="server" 
                    text="<%$Resources:wss,login_pageUserName%>" 
                    EncodeMethod='HtmlEncode'/>
            </td>
            <td width="100%">
                <asp:textbox id="UserName" 
                    autocomplete="off" 
                    runat="server" 
                    class="ms-inputuserfield" width="99%" />
            </td>
        </tr>
        <tr>
            <td nowrap="nowrap">
                <SharePoint:EncodedLiteral runat="server" 
                    text="<%$Resources:wss,login_pagePassword%>" 
                    EncodeMethod='HtmlEncode'/>
            </td>
            <td width="100%">
                <asp:textbox id="password" TextMode="Password" 
                    autocomplete="off" runat="server" 
                    class="ms-inputuserfield" width="99%"/>
            </td>
        </tr>
        <tr>
            <td nowrap="nowrap">
                <SharePoint:EncodedLiteral runat="server" 
                    text="Secure Code:" EncodeMethod='HtmlEncode'/>
            </td>
            <td width="100%">
                <asp:textbox id="secureCode" autocomplete="off" 
                    runat="server" class="ms-inputuserfield" Width="85%" />
                <SharePoint:EncodedLiteral ID="secureCodeLit" 
                    runat="server" Text="1234" EncodeMethod="HtmlEncode" />
            </td>
        </tr>
        <tr>
            <td colspan="2" align="right">
                <asp:button id="login" commandname="Login" 
                    text="<%$Resources:wss,login_pagetitle%>" runat="server" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <asp:checkbox id="RememberMe" 
                    text="<%$SPHtmlEncodedResources:wss,login_pageRememberMe%>" 
                    runat="server" />
            </td>
        </tr>
        </table>
    </layouttemplate>
 </asp:login>

What I want to do is only when users input the correct user name and password with the Secure Code, that is 1234 in this demo, they will allowed to access the site. When browsing the site, its login page looks like the following:

image

Of course the Secure Code does not function now because the build-in sign in page does not know how to handle it. I have to create my own module to proceed the secure code as well as the sign in logic.

Creating a Code Behind Class to Handle the Request of the Sign-in Page

Now its time to write some code to handle the sign-in request. In the project, add a new class and name it to, for example, FormsSignInPage. At this point, we have to reference to some necessary assemblies. This is a page for SharePoint site, so the first assembly we must reference to is Microsoft.SharePoint.dll. Since we are dealing with Claims, System.IdentityModel.dll and Microsoft.IdentityModel.dll are necessary as well. SharePoint has its own module to deal with Claims too. It is Microsoft.SharePoint.IdentityModel.dll. To reference to it, we have to browse to its location and add it. The assemblies referenced in my project look like the following:

image

The class, FormsSignInPage, can derive from the Page class of the Asp.Net. However, if you check the build-in sign-in page, it derives from IdentityModelSignInPageBase which is under Microsoft.SharePoint.IdentityModel.Pages namespace. It provides some benefits for signing in with claims. So I just made my FormsSignInPage derive from this base class too.

When users click Sign In button, I handle two events of the Login control, LoggingIn and Authenticate. In LoggingIn event, I check whether the correct Secure Code has been input. If not, I just cancel the sign-in. In Authenticate events, the actual authentication action is performed.

 protected void signInControl_LoggingIn(object sender, LoginCancelEventArgs e)
{
    LoginControl login = sender as LoginControl;
    login.UserName = login.UserName.Trim();
    if (string.IsNullOrEmpty(login.UserName))
    {
        ClaimsFormsPageMessage.Text = "The server could not sign you in. The user name cannot be empty.";
        e.Cancel = true;
    }
    if (string.IsNullOrEmpty(secureCode.Text) || 
        !string.Equals(secureCode.Text.ToLower(), secureCodeLit.Text.ToLower()))
    {
        ClaimsFormsPageMessage.Text = "The server could not sign you in. Please input correct secure code.";
        e.Cancel = true;
    }
}
 private void EstablishSessionWithToken(SecurityToken securityToken)
{
    if (null == securityToken)
    {
        throw new ArgumentNullException("securityToken");
    }
    SPFederationAuthenticationModule fam = SPFederationAuthenticationModule.Current;
    if (null == fam)
    {
        throw new ArgumentException(null, "FederationAuthenticationModule");
    }

    fam.SetPrincipalAndWriteSessionToken(securityToken);
}

protected void signInControl_Authenticate(object sender, AuthenticateEventArgs e)
{
    SecurityToken token = null;
    LoginControl formsLoginControl = sender as LoginControl;

    if (null != (token = GetSecurityToken(formsLoginControl)))
    {
        EstablishSessionWithToken(token);
        e.Authenticated = true;
        base.RedirectToSuccessUrl();
    }
}
 private SPIisSettings IisSettings
{
    get
    {
                
        SPWebApplication webApp = SPWebApplication.Lookup(new Uri(SPContext.Current.Web.Url));
        SPIisSettings settings = webApp.IisSettings[SPUrlZone.Default];
        return settings;
    }
}

private SecurityToken GetSecurityToken(LoginControl formsLoginControl)
{
    SecurityToken token = null;
    SPIisSettings iisSettings = IisSettings;
    Uri appliesTo = base.AppliesTo;

    if (string.IsNullOrEmpty(formsLoginControl.UserName) ||
        string.IsNullOrEmpty(formsLoginControl.Password))
        return null;

    SPFormsAuthenticationProvider authProvider = iisSettings.FormsClaimsAuthenticationProvider;
    token = SPSecurityContext.SecurityTokenForFormsAuthentication(
        appliesTo,
        authProvider.MembershipProvider,
        authProvider.RoleProvider,
        formsLoginControl.UserName,
        formsLoginControl.Password);

    return token;
}

Key part of our page is how to perform sign-in action. SharePoint provide us a function called SecurityTokenForFormsAuthentication which is used to perform Forms authentication. The code I am using is below. I get the membership provider and roleship provider from the SPIisSettings.

 SPFormsAuthenticationProvider authProvider = iisSettings.FormsClaimsAuthenticationProvider;
token = SPSecurityContext.SecurityTokenForFormsAuthentication(
    appliesTo,
    authProvider.MembershipProvider,
    authProvider.RoleProvider,
    formsLoginControl.UserName,
    formsLoginControl.Password);

Making a Connection Between Default.aspx and FormsSignInPage

The final step is to make a connection between default.aspx and FormsSignInPage. To make it, we just need to modify the <%@ Page %> of the default.aspx like this:

 <%@ Page Language="C#" AutoEventWireup="true" 
    Inherits="Morpheus.Demo.Pages.FormsSignInPage,FormsSignInPage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=72d2bbe72853b8eb" 
    MasterPageFile="~/_layouts/simple.master" %> 

Then we can deploy the assembly of our code to GAC and the default.aspx to _forms folder of our web app. When signing in with our custom page, without inputting Secure Code correctly, users will get an error like following.

image

I've shared the source code of my project here. And the Chinese version of this article can be found here.

FormsSignInPage.zip