Tuesday, December 07, 2010

Nicer Claims Login Page SharePoint 2010

I have been working on a Forms Based Authentication (FBA) solution for an internet site and one of the requirements was the creation of a custom login page, one that will be somewhat more user friendly than the drop down page that SharePoint 2010 uses.

Most internet users will have no clue what to do with the "choose your claims provider" drop down so we wanted to show a page with a username and password box where most users will enter their credentials. These will be authenticated against the SQL database backed FBA, exactly what needs to happen for internet users. However we also want to show a small link that will automatically log in our internal users with Windows authentication.



This is not too difficult but I can see myself needing this again thus the blog post.

I started with following this blog post on Creating a Custom Login Page for SharePoint 2010 by Kirk Evans, which worked great. Following his instructions gets you to the point of having a login page for your FBA users. All you need now is that link for your internal users.

First I modified the login control to make sure it goes against the correct Membership Provider, by adding the MembershipProvider attribute like:

<asp:Login ID="signInControl" FailureText="<%$Resources:wss,login_pageFailureText%>" MembershipProvider="FBAMembershipProvider"
        runat="server" Width="100%" DisplayRememberMe="false" />

To be honest I'm not sure if that is needed, it just seemed logical to me.

Next I added a LinkButton right under the login control so I had markup like:

<asp:Login ID="signInControl" FailureText="<%$Resources:wss,login_pageFailureText%>" MembershipProvider="FBAMembershipProvider"
        runat="server" Width="100%" DisplayRememberMe="false" />
<asp:LinkButton ID="hlInternalUsers" Text="Internal Login" runat="server" />

Lastly I added some simple code to the code beside file: (explanation below)

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            hlInternalUsers.Click += new EventHandler(hlInternalUsers_Click);
        }

        void hlInternalUsers_Click(object sender, EventArgs e)
        {
            if (null != SPContext.Current && null != SPContext.Current.Site)
            {
                SPIisSettings iisSettings = SPContext.Current.Site.WebApplication.IisSettings[SPUrlZone.Default];
                if (null != iisSettings && iisSettings.UseWindowsClaimsAuthenticationProvider)
                {
                    SPAuthenticationProvider provider = iisSettings.WindowsClaimsAuthenticationProvider;
                    RedirectToLoginPage(provider);
                }
            }
        }

        //borrowed from Microsoft.SharePoint.IdentityModel.LogonSelector
        private void RedirectToLoginPage(SPAuthenticationProvider provider)
        {
            string components = HttpContext.Current.Request.Url.GetComponents(UriComponents.Query, UriFormat.SafeUnescaped);
            string url = provider.AuthenticationRedirectionUrl.ToString();
            if (provider is SPWindowsAuthenticationProvider)
            {
                components = EnsureUrlSkipsFormsAuthModuleRedirection(components, true);
            }
            SPUtility.Redirect(url, SPRedirectFlags.Default, this.Context, components);
        }

        //borrowed from Microsoft.SharePoint.Utilities.SPUtility
        private string EnsureUrlSkipsFormsAuthModuleRedirection(string url, bool urlIsQueryStringOnly)
        {
            if (!url.Contains("ReturnUrl="))
            {
                if (urlIsQueryStringOnly)
                {
                    url = url + (string.IsNullOrEmpty(url) ? "" : "&");
                }
                else
                {
                    url = url + ((url.IndexOf('?') == -1) ? "?" : "&");
                }
                url = url + "ReturnUrl=";
            }
            return url;
        }


The link button has an event handler attached to its Click event. The event handler makes sure there is an SPContext to work with and then gets an SPIisSettings object for the Default Zone of the current site. You can change the zone here if needed. If the SPIisSettings object is succesfully retrieved, we can check if Windows Authenticatoin is being used by this zone. If not, our link makes no sense! Next we get the Windows Authentication provider and call a helper method that will redirect the user to the right place.

The RedirectToLoginPage method is actually borrowed from the Microsoft.SharePoint.IdentityModel.LogonSelector class that is the control responsible for the drop down on the Out-of-box Claims logon page. As far as I can tell it plays with the urls so that the user is redirected to the correct login page (depending on the provider). Notice that this method uses yet another helper method, the EnsureUrlSkipsFormsAuthModuleRedirection method. This is a method borrowed from the SPUtility class, sadly it is internal so I just copied it out of reflector. You can use reflection if you want but I didn't want the performance hit. It just helps with some url magic.

So with a few lines of our own code and a few borrowed thanks to reflector, we have a login page that is very simple for our internet FBA users but still allows our internal users to log in with Windows Auth by clicking a simple link.

19 comments:

Adnan said...

thank you. Saved my life.

Travis Trout said...

I'm not sure how this will work? Coming to an anonymous login the SPContext.Current will always be null. How do you get around this?

Thank you

Joe said...

@Travis, the SPContext.Current is not null for anonymous users. Even if the user is not authenticated,there is information about the current SPSite and SPWeb that they are browsing for instance. I can assure you this code works, it is currently being used by a customer of mine.

Travis Trout said...

@Joe, I'm wondering if because I'm not inheriting from a masterpage if that's why I'm always NULL? I ended up using just these 3 lines of your code and got where I needed:

string components = HttpContext.Current.Request.Url.GetComponents(UriComponents.Query, UriFormat.SafeUnescaped);
components = EnsureUrlSkipsFormsAuthModuleRedirection(components, true);
SPUtility.Redirect("/_windows/default.aspx", SPRedirectFlags.Default, this.Context, components);

Thank you,
Travis

Travis Trout said...

@Joe, Sorry, looks like my Windows 7 environment is the culprit. I packaged up my solution and deployed to a windows server test environment and it works. Sorry to doubt your work :)

Thank you,
Travis Trout

Tina said...

I added "_forms/default.aspx" as the url for the custom login page. It nagivates right to the fba login page.

Ton Stegeman said...

Nice one Joe! Took me too much time to figure it our, but thanks to your post I got it working fine now!

Alexander Schulz said...

Thank you very much, Joe. You saved my day. Greetings from Cologne, Alex

Hernan said...

Is there anyway to use the signInControl.UserName and signInControl.Password to windows validation and login ?
Thanks

Joe said...

@Hernan, I don't think so since windows authentication is integrated in the browser but I could be wrong.

Tobias said...

Hi Joe,

Just wanted to say thanks...it's not often you find such elegant solutions on blogs, but after reading your article - five minutes later it's working for me

Thanks

Tobias

carbosound1 said...

Does anyone know why my SPContext.Current is always null?

Thanks in advance

carbosound1 said...

My appoligies.
Great Post.

I had to change the url in the custom login page to "~/sites/site/_layouts/login/login.aspx"

surabhisusheel said...

hi joe,
i tried to use ur code to create a custom login page but i am getting an error 404 file not found can u help me out regarding this issue.


regards,
susheel

Ashwith Karkera said...

Hi,

Thank you for the post! Looks this is almost close to what am looking for.
Can you please tell me that how can i use this code if I want to do only Windows authentication using similar login screen? Actually I dont want to use FBA since I dont have any external users. My users are in active directory and they should access site from internal(office) and also from internet(outside office)(by extending). So whenever I browse extended windows authentication claims based site from internet, I should be prompted for windows AD credentials. Upon valid credential, home page should get loaded.

Thanks,
AK

Joel Lawler said...

Thanks for posting this.

I am having an odd issue. When I point my provider to the custom login page the site pops a Windows Security box.

If I login through the Windows Security, I can get to my custom login screen.

If I remove the custom login, I get the default login with the Windows / Forms drop-down.

Any help would be much appreciated.

Thanks

Joe said...

@Ashwith I don't think this is the right solution for your problem. There is a thread here that describes your problem, perhaps that can help. http://social.technet.microsoft.com/Forums/en-US/sharepointgeneral/thread/cc04e572-040b-4238-923e-fa72f2efd810/

Joe said...

@Joel Not sure what could be causing your custom login page to throw up a login box, but I would guess that there is either something on the page that an unauthenitcated user cannot do (think code that should be wrapped in SPSecurity.RunWithElevatedPrivileges) or that the page itself is not seen as anonymously accesible bu IIS. Start by commenting out (not HTML but ASP comments) all the controls on your custom login page to see if it shows then. If so, it's the code. Otherwise look in the IIS logs and see what 40X.X code you are getting back and go from there. See this page for some help http://blog.crowe.co.nz/archive/2005/08/26/231.aspx

Jaime Bennett said...
This comment has been removed by the author.