Writing A Custom Forms Login Page for SharePoint 2010 Part 1

In SharePoint 2007 writing a custom login page for a forms based authentication (FBA) site was not too terribly hard. There were a few things to know, most of which weren’t SharePoint specific, and some tips to have your login form take on the look and feel of a standard SharePoint layouts page. Overall though, if you knew ASP.NET and the FormsAuthentication class you were good to go. As luck would have it, things get somewhat more complicated in SharePoint 2010.

In this post we’ll walk through one scenario of a custom login page. In this example we decide that we need an entirely custom login page – we’re not just changing the look and feel, we require an entirely different UI. For example, maybe we need to grab the Membership credentials that are used to log in, and then maybe we need someone to enter a secondary authentication ID, like you might have with SecurID. In that case we’re going to have a couple of text boxes on an ASP.NET page for username and password and we’ll need to take those and programmatically log our user in.

The most important point to remember here is that your old friend, the FormsAuthentication class, will no longer be used. The reason for that is because in SharePoint 2010, FBA users are actually claims users. So while you may think you’re working with a standard ASP.NET Membership user and Role provider, under the covers those objects have a shiny claims authentication shell. Because of that, we need to use some of the SharePoint claims classes to work through the FBA login process.

I need to add one caveat here! Normally through one way or another, before I post something on my blog I either know or validate as best I can with someone that yeah, this is the right / blessed / supported way of doing something. In this case, I tried repeatedly to get this approach vetted but was unable to do so. I have used this code for a project I was working on and it does work, but I don’t want someone going into cardiac arrest if the code police tell you at a later date that you need to modify it to meet some other better / more appropriate way of doing things. So enough with the CYA, let’s just look at some code.

Before we get started there’s a couple of references we’re going to need that you probably haven’t used before. The first one is Microsoft.SharePoint.Security.dll, and it’s in the 14 hive in the ISAPI folder. The other one is trickier, and primarily why I gave my caveat above. You need a reference to Microsoft.SharePoint.IdentityModel.dll. However, if you go to add references you won’t readily find this assembly, and thus my source of being slightly embarrassed and wary at the same time. As I described in another post, the best thing I found to do is find it on the file system, copy it to an easy to find location, and add your reference to the copied version. The way I usually do that, ‘cause I’m old school I guess, is I go to a command prompt, change to the root of the drive and do a “dir Microsoft.SharePoint.IdentityModel.dll /s” and find it that way. Once you have that, you’ll probably want to add a little gaggle of using statements:

using System.Web.Security;

using System.IdentityModel.Tokens;

using Microsoft.SharePoint;

using Microsoft.SharePoint.IdentityModel;

So now that we have that bit of awkwardness out of the way, when I call into the claims class to validate the FBA user credentials the user typed in I need to tell it what Membership and Role provider it should use. It just needs a name is all. In my particular case I had written a custom Membership and Role provider for what I was doing, so I just enumerated through all the providers my web application knew about until I found mine:

//get the provider names for our type

string userProviderName = string.Empty;

string roleProviderName = string.Empty;

 

//get the membership provider name

foreach (MembershipProvider p in Membership.Providers)

{

if (p.GetType().Equals(typeof(Microsoft.SE.AnonProvider.Users)))

       {

       userProviderName = p.Name;

              break;

       }

}

 

//get the role provider name

foreach (RoleProvider rp in System.Web.Security.Roles.Providers)

{

if (rp.GetType().Equals(typeof(Microsoft.SE.AnonProvider.Roles)))

       {

       roleProviderName = rp.Name;

              break;

       }

}

 

Okay, great, I got my provider names. Now we need to take the username and password and get back a SecurityToken. In order to do that, we’re going to use the SPSecurityContext class. It has a method designed just to do this forms based auth login for us; if it’s successful it returns a SecurityToken – if not it returns null. Here’s what it looks like when we authenticate the user credentials:

SecurityToken tk = SPSecurityContext.SecurityTokenForFormsAuthentication(

new Uri(SPContext.Current.Web.Url), userProviderName, roleProviderName,

          UserNameTxt.Text, PasswordTxt.Text);

 

So I’ve passed in a Uri for the site I’m trying to authenticate against, I’ve told it the name of my membership and role providers, and I’m passing in the username and password values that were typed in the textboxes in my login page. Now I need to check to make sure that my SecurityToken is not null, and if it isn’t I need to write a session token. That is done with the SPFederationAuthenticationModule. Once I’ve written my session token then I can go ahead and redirect the user to whatever page or resource it was that they requested. Here’s the rest of the code that does that:

if (tk != null)

{

//try setting the authentication cookie

SPFederationAuthenticationModule fam = SPFederationAuthenticationModule.Current;

fam.SetPrincipalAndWriteSessionToken(tk);

 

       //look for the Source query string parameter and use that as the redirection

       string src = Request.QueryString["Source"];

       if (!string.IsNullOrEmpty(src))

       Response.Redirect(src);

}

else

{

StatusLbl.Text = "The credentials weren't valid or didn't work or something.";

}

 

Now you see when I’ve successfully done everything then I just grab the Source query string parameter because it tells us where the user was originally headed. Once I have that I just send them on their way.

Hopefully this helps you get going. I know it was a real struggle to find the documentation on how best to do this when I was looking for it. In part 2 we’ll look at doing this a different way for a different scenario. In that well want to have someone sign an “I agree to the terms of use for this website” thing before they use the site the first time. To do that we’ll look at extending the base login page and adding a handler at login time.