The users are impatient (and sometimes they should be). Help them if you can.
2 votes, 6.00 avg. rating (100% score)

Collaboration is fun, and the saying: “the more, the merrier” really do applies when it comes with working with contents in SharePoint. Not saying that everyone should do everything, but the more people that have a chance to be a part of a collaboration process the better.

In many scenarios it is not well suited to have all users in the company active directory, instead custom memebership providers are used. Back in the MOSS 2007 days, form based authentication (FBA) was managed by SharePoint itself. Now with SharePoint 2010 it all relies on claims, a part of the Windows Identity Foundation (to read more about WIF, read the excellent whitepaper about claims architecture by David Chappel, and if you want to know what a claim looks like read this blog post by Wictor Wilén). I will not cover any more in this post about why and when you should use FBA. The focus here is what to do when you have already made your architectural decision.

Something that I often find useful when it comes to implementing a custom membership provider solution is the possibility to administer when a user is asked to provide a valid windows identity, and when a user should be prompted for FBA credentials.

By providing this for the user, we can get rid of this default page:

To be able to eliminate that first page is great in a user experience point of view. Just think about it; do a user really care about what type of authentication to use? Does the users even know the difference? Should they?

So why is there even the need for a sign-in page where you select authentication? This is how it works with the default settings:

  1. When a user that is not authenticated connects to a site with multiple authentication methods SharePoint redirects the user to a page containing the authentication mode selector.
  2. When the user selects authentication method the user is then redirected to a page where the real authentication happens.

The pages that manages this is located in the 14\TEMPLATE\IDENTITYMODEL folder. There are four subfolders there:

  • LOGIN – Containing the page mentioned in step 1 above. Inherits from Microsoft.SharePoint.IdentityModel.Pages.MultiLogonPage.
  • FORMS – Containing a standard page for Forms Based Authentication. Inherits from Microsoft.SharePoint.IdentityModel.Pages.FormsSignInPage.
  • WINDOWS – Containing a standard page for Windows Authentication. Inherits from Microsoft.SharePoint.IdentityModel.Pages.WindowsSignInPage.
  • TRUST – Containing a standard page for trusted providers such as ADFS (Active Directory Federation Services). Inherits from Microsoft.SharePoint.IdentityModel.Pages.TrustedProviderSignInPage.

When I build my own replacement for these files I place them in their respective folder with a new name. I never overwrite the default.aspx file, even though it might be tempting, because then you don’t have to cutomize your own url’s. But it is a bad practice since they can be overwritten by SharePoint in the case of service packs and/or updates.

The Task

A common way to determine how a user should be authenticated is their location. In the case of a collaboration portal, users that are on-site will probably have a windows account. People outside needs to authenticate using FBA. And we determine this by comparing their IP-address with a known range of internal IP-addresses. We also want to package this in a Feature that automatically updates the Sign In-page property for the web application, and puts a new value (our IP-range) in web.config.

The Solution

Fire up your Visual Studio and begin with an empty SharePoint 2010 project. Name it to something smashing (or follow a proper naming convention, whichever you prefer). I used the simple name MyCustomSignInPage for my project name in this sample.

  • Add a mapping to the TEMPLATE\IDENTITYMODEL\LOGIN-folder.
  • Add a reference to System.Configuration (we will need this to read values from the AppSettings in web.config).
  • Add a reference to Microsoft.SharePoint.IdentityModel. If it is the first time you use it you need to browse to it. It is located here:
    C:\Windows\assembly\GAC_MSIL\Microsoft.SharePoint.IdentityModel\14.0.0.0__71e9bce111e9429c\Microsoft.SharePoint.IdentityModel.dll

Create a new textfile in the newly mapped folder and rename it CustomLogin.aspx and add the following markup:

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="CustomLogin.aspx.cs"
    Inherits="MyCustomSignInPage.LOGIN.CustomLogin" %>

That’s it with the markup. Everything else we will do is in the code behind class.

Create the code behind file, CustomLogin.aspx.cs,  and inherit from IdentityModelSignInPageBase. This might seem a bit odd, since the page Microsoft have placed in LOGIN inherits from MultiLogonPage. However that one needs a little bit of plumbing since it is supposed to interact with the user. We don’t want to do that, we only want to redirect the user so we do as the cool chefs; we take the fast lane to save time. The IdentityModelSignInPageBase has all the stuff we need already in the base class  (very convenient, right?).

Add a Page_Load-method and a method that takes a string value and returns a long, named to IpStringToLong. The latter method will help us to determine whether a user is within the correct IP-range. The code behind class will look something like this:

using Microsoft.SharePoint.IdentityModel;

namespace MyCustomSignInProject.LOGIN
{
    public partial class CustomLogin : IdentityModelSignInPageBase
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            // We will implement code here
        }

        // Takes an IP-string in the xxx.xxx.xxx.xxx
        // and returns it as long.
        private long IpStringToLong(string ipString)
        {
           // We will implemet code here
        }
    }
}

First we want to get the IP-number of the user, and we want it in IPv4-format. We do this by asking the DNS for the InterNetwork type like so:

string callerIp;

if (Request.UserHostAddress!=null)
{
   foreach (var ipAddress in
            Dns.GetHostAddresses(Request.UserHostAddress).
            Where(ipAddress => ipAddress.AddressFamily == AddressFamily.InterNetwork))
   {
      callerIp = ipAddress.ToString();
      break;
   }
}

Now we read the value RedirectRange (a string in the format: xxx.xxx.xxx.xxx-xxx.xxx.xxx.xxx) from web.config (we will set this value in our feature event later) with:

var ipRangeString = ConfigurationManager.AppSettings["RedirectRange"];

To check if the IP-number we have are in the given range we simply convet our IP-strings to long and then see if the callers IP-address is larger than the first value, and lower than the second. Very neat.

double num = 0;
if (!string.IsNullOrEmpty(ip))
{
    var ipBytes = ip.Split('.');
    for (var i = ipBytes.Length - 1; i >= 0; i--)
    {
        num += ((int.Parse(ipBytes[i]) % 256) * Math.Pow(256, (3 - i)));
    }
}
return (long)num;

Let us now check to see if the user is within range with our new method:

var ipRange = ipRangeString.Split('-');
var isCallerWithinRange = 
        IpStringToLong(ipRange[0]) <= IpStringToLong(callerIp) &&
        IpStringToLong(callerIp) <= IpStringToLong(ipRange[1]); 

Now we implement the code to redirect the user to windows authentication if there is a range match, and forms authentication if not. When we have the correct redirect url, we send the user on its way with our best regards.

// Get a IEnumerable of the ClaimsAuthenticationProviders in the current WebApp
var spWebApp = SPContext.Current.Site.WebApplication;
var iisSettings = spWebApp.IisSettings[SPUrlZone.Default];

// Get the Redirect URL for the claimtype
var urlValue = isCallerWithinRange ?
    iiSsettings.WindowsClaimsAuthenticationProvider.
        AuthenticationRedirectionUrl.ToString()
    :
    iiSsettings.FormsClaimsAuthenticationProvider.
        AuthenticationRedirectionUrl.ToString();

// What page did the user first request?
var requestedUrl =
    Request.Url.GetComponents(UriComponents.Query, UriFormat.SafeUnescaped)

// Now we have all that we need. Send the user on its way!
SPUtility.Redirect(
    urlValue,                    // Redirect URL
    SPRedirectFlags.Default,     // Redirect flags
    Context,                     // Our http context
    requestedUrl);               // The user's requested page

Now add a new Web Application-scoped feature and add an event receiver. Add the following code to modify the Sign In-page settings and add our IP-range value to web.config (the Sign In-page settings is also placed in web.config by SharePoint).

var spWebApp = (SPWebApplication)properties.Feature.Parent;

// Get the IIS Settings for the default zone
var iisSettings = spWebApp.IisSettings[SPUrlZone.Default];

// Set our new Sign In-page as default.
iisSettings.ClaimsAuthenticationRedirectionUrl =
    new Uri("CustomLogin.aspx", UriKind.Relative);
spWebApp.Update();

// Add our settings value to web.config
var webConfigMod = new SPWebConfigModification()
    {
        Name = "add[@key='RedirectRange']",
        Path = "configuration/appSettings",
        Owner = "CustomLoginPage",
        Sequence = 0,
        Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
        Value = "<add key='RedirectRange' value='158.127.0.0-158.127.255.255' />"
    }

// Apply the web.config modificatoin
spWebApp.WebConfigModifications.Add(webConfigMod);
spWebApp.Update();
spWebApp.WebService.ApplyWebConfigModifications();

Remember to set your own ip-range value. Also add logic to clean up your web.config if the feature is deactivated.

That’s it!

Deploy your solution, and the users that are within your set range is automatically authenticated using windows authentication, and users outside the rage uses FBA.

Summary

Improving the user experience is very important when it comes to application adaptation. If there is a possibility to make their lives easier, we should do it. That is our goal.

Remember that this solution only addresses the default zone. If you have more zones implemented in your solution, you may need to modify this code.

Happy coding, and happy users!