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.
Comments
Anonymous
January 01, 2003
The comment has been removedAnonymous
July 25, 2010
Hi, I have done all above steps & working fine now but I have a little requirement. I would like to take some decision on source querystring parameter witn in the membership class, you can say within the validate method. So how I can get that with the membership class?Anonymous
July 30, 2010
A perhaps simpler, and less "awkward" way of getting the forms provider would be: var settings = site.WebApplication.IisSettings[site.Zone]; MembershipProvider fbaProvider = Membership.Providers[settings.FormsClaimsAuthenticationProvider.MembershipProvider]; Hope this is of some help.Anonymous
August 08, 2010
I require the functionality i.e user sign an “I agree to the terms of use for this website” thing before they use the site the first time. Please help it's urgentAnonymous
August 11, 2010
Hi, when I use the correct user and pass I get "Object reference not set to an instance of an object" error. However when I use an incorrect user and pass the token is always NULL. It seems that method recognize the user and pass as valid but cannot issue the security token. Could you please help. IvanAnonymous
August 11, 2010
Hi, In addtion to my last post, I noticed that when i browse to 192.168.15.27/.../securitytoken.svc I get the following error: System.InvalidOperationException: An exception was thrown in a call to a policy export extension. Extension: System.ServiceModel.Channels.TransportSecurityBindingElement Error: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider IvanAnonymous
August 31, 2010
when i browse to http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc I get the following error: An ExceptionDetail, likely created by IncludeExceptionDetailInFaults=true, whose value is: System.InvalidOperationException: An exception was thrown in a call to a policy export extension. Extension: System.ServiceModel.Channels.TransportSecurityBindingElement Error: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider. Policy export for such a binding is not supported. Make sure the transport binding element in the binding implements the ITransportTokenAssertionProvider interface. ----> System.InvalidOperationException: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider. Policy export for such a binding is not supported. Make sure the transport binding element in the binding implements the ITransportTokenAssertionProvider interface. at System.ServiceModel.Channels.TransportSecurityBindingElement.System.ServiceModel.Description.IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext policyContext) at System.ServiceModel.Description.MetadataExporter.ExportPolicy(ServiceEndpoint endpoint) --- End of inner ExceptionDetail stack trace --- at System.ServiceModel.Description.ServiceMetadataBehavior.MetadataExtensionInitializer.GenerateMetadata() at System.ServiceModel.Description.ServiceMetadataExtension.EnsureInitialized() at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.InitializationData.InitializeFrom(ServiceMetadataExtension extension) at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.GetInitData() at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.TryHandleDocumentationRequest(Message httpGetRequest, String[] queries, Message& replyMessage) at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.ProcessHttpRequest(Message httpGetRequest) at SyncInvokeGet(Object , Object[] , Object[] ) at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs) at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc) at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)Anonymous
September 17, 2010
Hi, thanks for this article. It's helped me alot. I was wondering if you could possibly answer a question I have. I'm planning on running mixxed authentication (windows and forms based) adn would like to add to your solution, however, I'm having troubles figuring out how to issue the SecurityToken when authenticating against a WindowsClaimsAuthenticationProvider. You provide the way to get a SecurityToken when authenticating a FormsClaimsAuthenticationProvider. Could you shed light on how to get one when using a WindowsClaimsAuthenticationProvider please? Thank you if you can! :-)Anonymous
November 07, 2010
The comment has been removedAnonymous
December 09, 2010
Good article. How to do same for Windows Authentication. Thanks in advance.Anonymous
January 08, 2011
The comment has been removedAnonymous
April 05, 2011
where to find lib dll for Microsoft.SE.AnonProvider ? thanksAnonymous
April 05, 2011
I used below code and can't find my custom membership provider. It only find default provider. Any idea? From sharepoint central admin, it can access my custom membership provider and I have assigned some users to my web application. I can use these users to login using default form login. But my custom login page is not working. thanks. Gary foreach (MembershipProvider p in Membership.Providers) { if (p.GetType().Equals(typeof(Microsoft.SE.AnonProvider.Users))) { userProviderName = p.Name; break; } }Anonymous
September 18, 2014
The comment has been removedAnonymous
November 13, 2014
The comment has been removedAnonymous
February 28, 2015
In part 1 of this series, which you can find at http://blogs.technet.com/b/speschka/archive/2010/07/21Anonymous
February 28, 2015
In part 1 of this series, which you can find at http://blogs.technet.com/b/speschka/archive/2010/07/21Anonymous
March 01, 2015
SIDE NOTE: Yet another kudos to the fabulous folks that run this site. This latest version now retains