6

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.

<%@ Assembly Name="Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharepointIdentity" Namespace="Microsoft.SharePoint.IdentityModel" Assembly="Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Page Language="C#" Inherits="MyCustomSignInModule.SignInForm, MyCustomSignInModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f2ae721af94bf9e9" MasterPageFile="~/_layouts/15/errorv15.master"%>
<%@ Import Namespace="Microsoft.SharePoint.WebControls" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<asp:Content ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" runat="server">
  <SharePoint:EncodedLiteral runat="server"  EncodeMethod="HtmlEncode" Id="ClaimsLogonPageTitleInTitleArea" />
</asp:Content>


<asp:Content ContentPlaceHolderId="PlaceHolderPageTitle" runat="server">
  <SharePoint:EncodedLiteral runat="server"  EncodeMethod="HtmlEncode" Id="ClaimsFormsPageTitle" />
</asp:Content>
<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
  <SharePoint:EncodedLiteral runat="server"  EncodeMethod="HtmlEncode" Id="ClaimsLogonPageMessage" />
  <br />
  <br />
  <SharepointIdentity:LogonSelector ID="ClaimsLogonSelector" runat="server" />

 <div id="SslWarning" style="color:red;display:none">
 <SharePoint:EncodedLiteral runat="server"  EncodeMethod="HtmlEncode" Id="ClaimsFormsPageMessage" />
 </div>
  <script language="javascript" >
    if (document.location.protocol != 'https:') {
      var SslWarning = document.getElementById('SslWarning');
      SslWarning.style.display = '';
    }
  </script>
 <asp:login id="signInControl" FailureText="<%$Resources:wss,login_pageFailureText%>" runat="server" width="100%">
  <layouttemplate>
    <asp:label id="FailureText" class="ms-error" runat="server"/>
    <table width="100%">
    <tr>
      <td nowrap="nowrap"><SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,login_pageUserName%>" EncodeMethod='HtmlEncode'/></td>
      <td width="100%"><asp:textbox id="UserName" autocomplete="off" runat="server" class="ms-inputuserfield" width="99%" /></td>
    </tr>
    <tr>
      <td nowrap="nowrap"><SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,login_pagePassword%>" EncodeMethod='HtmlEncode'/></td>
      <td width="100%"><asp:textbox id="password" TextMode="Password" autocomplete="off" runat="server" class="ms-inputuserfield" width="99%"/></td>
    </tr>
    <tr>
      <td colspan="2" align="right"><asp:button id="login" commandname="Login" text="<%$Resources:wss,login_pagetitle%>" runat="server" /></td>
    </tr>
    <tr>
      <td colspan="2"><asp:checkbox id="RememberMe" text="<%$SPHtmlEncodedResources:wss,login_pageRememberMe%>" runat="server" /></td>
    </tr>
    </table>
  </layouttemplate>
 </asp:login>
 <asp:label id="DebugText" class="ms-error" runat="server"/>
</asp:Content>

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.

  • 1
    @Lankymart I took the time to test your code on my environment and it's working just fine. There must be something related to how you are deploying the solution (I don't understand how you can even develop on a machine without SP2013 installed) – MdMazzotti Apr 16 '14 at 11:51
  • 1
    @Lankymart I deployed directly from Visual Studio. Tomorrow if I have some spare time I will try to deploy with your method and let you know – MdMazzotti Apr 16 '14 at 21:39
  • 1
    @Lankymart here's a screenshot proving that SPContext.Current is not null (when deploying the solution from Visual Studio): s4.postimg.org/86ae7jwkt/loginpage.jpg – MdMazzotti Apr 20 '14 at 9:43
  • 1
    @Lankymart I just tested your deployment method (I built the DLL on a different machine whit no sharepoint installed, compiled and manually deployed). It is working fine. So I have no idea why it doesn't work on your environment. The code is correct – MdMazzotti Apr 20 '14 at 10:23
  • 1
    @Lankymart here's a screenshot proving that the custom signin page got called (I just commented out the RedirectToLoginPage(provider); line and added this DebugText.Text = "You are about to be redirected using WindowsClaims auth provider";) postimg.org/image/ttp2cwcgj – MdMazzotti Apr 20 '14 at 10:53
2

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.

  • 1
    I'm glad to see you finally found out what the problem was – MdMazzotti May 21 '14 at 16:30
  • @MdMazzotti Can't believe how simple it was, just sorry I didn't provide the site structure. Maybe then would have gotten to the bottom of the issue far quicker. Really appreciate your help though, was invaluable. – Lankymart May 21 '14 at 16:32
1

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?

  • Isn't the code above server side code, not javascript? – Robert Lindgren Apr 16 '14 at 8:59
  • Yeah it's c#, I didn't know if this question was a good fit for this site. Do you mean add the javascript in the ASPX page above the if (document.location.protocol != 'https:') { line? – Lankymart Apr 16 '14 at 9:01
  • Ah, it is indeed. Let my edit my answer. – Magali Apr 16 '14 at 9:01
  • Yes I did mean to add it in the ASPX page above that if-statement, sorry if I wasn't too clear. I sometimes forget to mention things. :) – Magali Apr 16 '14 at 9:07
  • 1
    @Magali the poster is developing on a remote machine and manually deploying to the server. I don't see how your suggestion applies in his case... – MdMazzotti Apr 16 '14 at 12:54
0

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.

  • Elevated Privileges was a workaround, even without it it still doesn't work for me. @MdMazzotti has already commented that he has deployed the code successfully and it works so it looks like it might be my method of remotely compiling the assembly and deploying it to the SharePoint Server manually. – Lankymart Apr 18 '14 at 11:06
0

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.

  • Appreciate your efforts but I have read that article previously and @MdMazzotti has proved that the code can be remotely compiled to a DLL and still work (see latest comments in the question). – Lankymart Apr 22 '14 at 11:25
  • 1
    The page inherits from the class declared inside the .DLL, so the fact that the DLL was compiled from a Class Library project doesn't really have any influence. As long as the DLL has been properly deployed to the GAC, where and how it was compiled is irrelevant – MdMazzotti Apr 22 '14 at 11:50
  • 1
    Plerse edit your first answer instead of posting multiple ones! – Robert Lindgren Apr 22 '14 at 12:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.