Question

Found the Solution

The solution was to add a Root Site Collection (as the code was running in a sub-site, without a root site).

See This Solution


Important Update (based on feedback received so far)

I do not have Visual Studio installed on the SharePoint 2013 Server. All code is compiled in Visual Studio 2012 remotely using references to the required SharePoint 2013 DLLs and digitally signed so it can be deployed into the Global Assembly Cache on the SharePoint 2013 Server.

At the most basic level this statement fails at point of Page_Load();

SPContext.Current.Site.WebApplication.GetIisSettingsWithFallback(
  SPContext.Current.Site.Zone
);

Because SPContext.Current is always null.

Background

Client needs there SharePoint site to be accessible using Form Based Authentication for external users and Windows Authentication for internal users (corporate). Have gone through and have setup both Authentication Providers and now get the default sign in.

Sign In

Although everything works Windows Authentication still displays the Provider selection screen when the client wants the Windows Authentication to just automatically pass through and display a Forms Based Authentication login screen if that fails.

Been struggling with this for days after looking at various blogs and articles I feel I'm no further along and that is depressing. At the moment I can't workout whether I'm missing something really fundamental.

Custom Sign In Page

The page is based off an amalgamation of;

  1. %CommonProgramFiles%\Microsoft Shared\Web Server Extensions\15\Template\IdentityModel\Login\default.aspx
  2. %CommonProgramFiles%\Microsoft Shared\Web Server Extensions\15\Template\IdentityModel\Forms\default.aspx

This is just a first attempt and realise I won't need all these controls eventually, just wanted to see how things hang together.

MyCustomSignInModule Assembly

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using Microsoft.IdentityModel.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.IdentityModel;
using Microsoft.SharePoint.IdentityModel.Pages;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Administration.Claims;
using Microsoft.SharePoint.ApplicationRuntime;
using Microsoft.SharePoint.Diagnostics;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
using System.IdentityModel.Tokens;
using System.Web.Configuration;

namespace MyCustomSignInModule
{
    public class SignInForm : FormsSignInPage
    {
        protected Label DebugText;

        private static SPIisSettings _iisSettings;

        internal static SPIisSettings iisSettings
        {
            get
            {
                if (_iisSettings == null)
                {
                    SPSecurity.RunWithElevatedPrivileges(delegate()
                    {
                        _iisSettings = SPContext.Current.Site.WebApplication.GetIisSettingsWithFallback(SPContext.Current.Site.Zone);
                    });
                }
                return _iisSettings;
            }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            HttpRequest request = HttpContext.Current.Request;

            bool isWindowsAuth = false;
            string username = request["username"];
            string password = request["password"];


            // If no username is provided, it'll probably be Windows Authentication (NTLMv2 protocol)
            if (String.IsNullOrEmpty(username))
            {
                isWindowsAuth = true;
            }

            try
            {

                //SPIisSettings iisSettings = SPContext.Current.Site.WebApplication.IisSettings[SPUrlZone.Default];

                if (isWindowsAuth)
                {
                    // Windows Authentication it is

                    if (iisSettings.UseWindowsClaimsAuthenticationProvider)
                    {
                        SPAuthenticationProvider provider = iisSettings.WindowsClaimsAuthenticationProvider;
                        RedirectToLoginPage(provider); // This should cause automatic sign in
                    }
                }
                else
                {
                    // FBA authentication it is.

                    SPFormsAuthenticationProvider formsClaimsAuthenticationProvider = iisSettings.FormsClaimsAuthenticationProvider;

                    SecurityToken token = SPSecurityContext.SecurityTokenForFormsAuthentication(new Uri(SPContext.Current.Web.Url), formsClaimsAuthenticationProvider.MembershipProvider, formsClaimsAuthenticationProvider.RoleProvider, username, password, SPFormsAuthenticationOption.PersistentSignInRequest);

                    if (null != token)
                    {
                        EstablishSessionWithToken(token);
                        base.RedirectToSuccessUrl();
                    }
                }
            }
            catch (Exception ex)
            {
                DebugText.Text = ex.ToString();
            }

        }

        // 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);
        }

        // 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;
        }

        // Microsoft.SharePoint.IdentityModel.Pages.IdentityModelSignInPageBase
        private void EstablishSessionWithToken(SecurityToken securityToken)
        {
            if (securityToken == null)
            {
                throw new ArgumentNullException("securityToken");
            }
            SPFederationAuthenticationModule fam = SPFederationAuthenticationModule.Current;
            if (fam == null)
            {
                throw new InvalidOperationException();
            }

            fam.SetPrincipalAndWriteSessionToken(securityToken);
        }

        private SPAuthenticationProvider
                GetAuthenticationProvider(string providerName)
        {
            SPIisSettings iisSettings =
                SPContext.Current.Site.WebApplication.IisSettings[SPUrlZone.Default];

            foreach (SPAuthenticationProvider currentProvider in
                        iisSettings.ClaimsAuthenticationProviders)
            {
                if (currentProvider.DisplayName.ToLower() ==
                            providerName.ToLower())
                {
                    return currentProvider;
                }
            }

            return null;
        }
    }
}

The Process

I have a test Sharepoint instance running on the VM but no Visual Studio installed as all the development is done remotely. The current process I follow;

  1. Build C# Class Library (this will be MyCustomSignInModule.Dll)
  2. Sign MyCustomSignInModule.Dll so it can be placed in the VM GAC.
  3. Register MyCustomSignInModule.Dll in the GAC on the VM instance.
  4. Save SignInForm.aspx into the %CommonProgramFiles%\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\ which is the equivalent of <SharePointSiteRoot>/_layouts/ virtual directory in IIS.
  5. In SharePoint Central Administration site set the Custom Sign In Page to my page ~/_layouts/SignInForm.aspx.
  6. Reset IIS using iisreset.
  7. Test accessing SharePoint site (this is where it falls over).

The Problem

Sign In Error

If it's not that clear the error is;

System.NullReferenceException: Object reference not set to an instance of an object. at
MyCustomSignInModule.SignInForm.b__0() at 
Microsoft.SharePoint.SPSecurity.<>c__DisplayClass5.b__3() at
Microsoft.SharePoint.Utilities.SecurityContext.RunAsProcess(CodeToRunElevated secureCode) at 
Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(WaitCallback secureCode, Object param) at 
Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(CodeToRunElevated secureCode) at MyCustomSignInModule.SignInForm.get_iisSettings() at 
MyCustomSignInModule.SignInForm.Page_Load(Object sender, EventArgs e) 

SPContext.Current is null why?

The NullReferenceException is being caused by SPContext.Current being null, but I can't workout why it's null and no amount of articles I've looked at have so far led to helping me solve this. I'm hoping by posting my whole process here someone might be able to point me to what I'm missing or doing wrong.

Was it helpful?

Solution

Finally got to the bottom of what was causing the issue, firstly would like to thank both @MdMazzotti and @Hugh Wood for their persistent help.

In the end I stumbled on the fix which turned out to be partly because the way the site was built. In my particular case the site was built as a sub-site in SharePoint, for whatever reason this meant there was no Site Collection at the root of the SharePoint website which was the cause of the Null SPContext in my code.

After adding a Root Site Collection my code began to work as expected.

OTHER TIPS

I'm just guessing here, but here are some things that I think could help.

Add this JavaScript code to the custom sign in page (before your if-statement):

SP.SOD.executeFunc('SP.js', 'SP.ClientContext');

You could also add this JavaScript to your page to test if it works or not, using the console:

var clientContext = new  SP.ClientContext.get_current(); 
console.log(clientContext);

If it returns something, then we have progress! If not, then I'll have to continue my search, since I find your question interesting.

EDIT

Instead of using this server-side code:

if (_iisSettings == null)
{
   SPSecurity.RunWithElevatedPrivileges(delegate()
   {
      _iisSettings = SPContext.Current.Site.WebApplication.GetIisSettingsWithFallback(SPContext.Current.Site.Zone);
   });
}
return _iisSettings;

Try using this instead:

if (_iisSettings == null)
{
   SPSecurity.RunWithElevatedPrivileges(delegate()
   {
      _iisSettings = Microsoft.SharePoint.SPContext.Current.Site.WebApplication.GetIisSettingsWithFallback(Microsoft.SharePoint.SPContext.Current.Site.Zone);
   });
}
return _iisSettings;

I searched around and noticed that in some of the code examples I encountered, the _iisSettings was different from yours. Perhaps that is why your SPContext.Current is null?

Please check this answer: https://stackoverflow.com/questions/5081251/how-can-this-throw-a-nullreferenceexception

SPContext.Current doesn't work under Elevated Privileges, so you have to rewrite like in the answer above.

Can you check this MSDN article: http://msdn.microsoft.com/en-us/library/office/ms468609(v=office.14).aspx

I think because you use a C# Class Library project in Visual studio, you are not in a web context and can't use SPContext.Current as explained in the MSDN article above.

Licensed under: CC-BY-SA with attribution
Not affiliated with sharepoint.stackexchange
scroll top