3

I am using formsauthentication on my MVC project and when testing locally using the Visual Studio Development Server everything works as expected. Once deployed to IIS 7.5 the HTTPContext.User is causing NullReferenceExceptions.

Both Dev and Prod machines are using the same SQL db (at the moment - this will change post-deployment of course) so I know it is not a problem with the DB or data within.

This must be a setting in IIS or my web.config but I cannot find it. I've tried various changes to my web.config(from suggestions I've found around SE), here is part of my web.config for the current implementation:

<appSettings>
    <add key="autoFormsAuthentication" value="true" />
    <add key="enableSimpleMembership" value="false" />
    <add key="webpages:Version" value="2.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="PreserveLoginUrl" value="true" />
    <add key="ClientValidationEnabled" value="true" />

****Snip****

<system.web>
    <httpRuntime targetFramework="4.5" />
    <compilation debug="true" targetFramework="4.5" />
    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2880" cookieless="UseCookies"/>
    </authentication>
    <pages>
      <namespaces>
        <add namespace="System.Web.Helpers" />
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization" />
        <add namespace="System.Web.Routing" />
        <add namespace="System.Web.WebPages" />
        <add namespace="ProjectSquid.WebUI.HTMLHelpers" />
      </namespaces>
    </pages>
    <roleManager enabled="true" defaultProvider="CustomRoleProvider">
      <providers>
        <clear />
        <add name="CustomRoleProvider" 
             type="Project.Domain.Filters.CustomRoleProvider"
             connectionStringName="EFDbContext"
             enablePasswordRetrieval="false"
             cacheRolesInCookie="true"/>
      </providers>
    </roleManager>
    <membership defaultProvider="SimpleMembershipProvider">
      <providers>
        <clear />
        <add name="SimpleMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData" />
      </providers>
    </membership>
    <sessionState mode="InProc" customProvider="DefaultSessionProvider">
      <providers>
        <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="EFDbContext" />
      </providers>
    </sessionState>

<system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="X-UA-Compatible" value="IE=9" />
      </customHeaders>
    </httpProtocol>
    <validation validateIntegratedModeConfiguration="false" />
    <handlers>
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
    <modules runAllManagedModulesForAllRequests="false">
      
      <remove name="FormsAuthentication" />
      <remove name="DefaultAuthentication" />
      <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="" />
      <add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="" />
      
      <remove name="UrlRoutingModule-4.0"/>
      <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" preCondition="" />
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
  </system.webServer>

What could cause HttpContext.User to differ from the VS Development Server and the IIS 7.5 implementation?

EDIT:

HttpContext is fed through the inherited BaseController:

protected virtual new CustomPrincipal User
{
    get { return HttpContext.User == null? null : HttpContext.User as CustomPrincipal; }
}

public new HttpContextBase HttpContext
{
    get
    {
        return ControllerContext == null ? null : ControllerContext.HttpContext;
    }
}

The cookie isn't created until the PostAuthenticationRequest:

 public void MvcApplication_PostAuthenticationRequest(object sender, EventArgs e)
{

    var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {

        string encTicket = authCookie.Value;
        if (!String.IsNullOrEmpty(encTicket))
        {
            
            var ticket = FormsAuthentication.Decrypt(encTicket);
            var id = new UserIdentity(ticket);
            string[] userRole = Roles.GetRolesForUser(id.Name);
            var prin = new CustomPrincipal(id);
            HttpContext.Current.User = prin;
            Thread.CurrentPrincipal = prin;
        }
    }
}

The authentication itself appears to be working fine as the function causing the exception starts with [Authorize] and successfully begins executing but fails as null when it reaches the first User reference:

int userT = User.Team.TeamId;

In this context the User being CustomPrincipal BaseController.User.

EDIT2:

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" timeout="2880" 
         cookieless="UseCookies"
         name=".ASPXAUTH"
         protection="All"
         slidingExpiration="true"/>
</authentication>

EDIT3

Custom IIdentity:

 [Serializable]
    public class UserIdentity : MarshalByRefObject, IIdentity
    {
        private readonly FormsAuthenticationTicket _ticket;


        public UserIdentity(FormsAuthenticationTicket ticket)
        {
            _ticket = ticket;
        }

        public string AuthenticationType
        {
            get { return "Custom"; }
        }

        public bool IsAuthenticated
        {
            get { return !string.IsNullOrEmpty(this.Name); }
        }

        public string Name
        {
            get { return _ticket.Name; }
        }

        public string UserId
        {
            get { return _ticket.UserData; }
        }

        public bool IsInRole(string Role)
        {
            return Roles.IsUserInRole(Role);
        }

        public IIdentity Identity
        {
            get { return this; }
        }


public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        if (context.State == StreamingContextStates.CrossAppDomain)
        {
            GenericIdentity gIdent = new GenericIdentity(this.Name, this.AuthenticationType);
            info.SetType(gIdent.GetType());

            System.Reflection.MemberInfo[] serializableMembers;
            object[] serializableValues;

            serializableMembers = FormatterServices.GetSerializableMembers(gIdent.GetType());
            serializableValues = FormatterServices.GetObjectData(gIdent, serializableMembers);

            for (int i = 0; i < serializableMembers.Length; i++)
            {
                info.AddValue(serializableMembers[i].Name, serializableValues[i]);
            }
        }
        else
        {
            throw new InvalidOperationException("Serialization not supported");
        }
    }

Custom IPrincipal:

interface ICustomPrincipal : IPrincipal
{
    int Id { get; set; }
    string Name { get; set; }
    string Role { get; set; }
}

public class CustomPrincipal : IPrincipal
{
    public CustomPrincipal(UserIdentity identity)
    {
        this.Identity = identity;
    }

    public IIdentity Identity { get; private set; }
abatishchev
  • 98,240
  • 88
  • 296
  • 433
mitchellJ
  • 734
  • 3
  • 9
  • 32
  • 1
    Does the project have extensionless pages? setting runAllManagedModulesForAllRequests=true might help in that case. Maybe this link would help http://forums.asp.net/t/1689878.aspx?HttpContext+Current+User+always+null+on+IIS+ – Pankaj Kumar Mar 23 '15 at 09:29
  • @PankajKumar Thanks, that does fix it but I've seen elsewhere that that's not the proper way to fix anything. Is there a better alternative that does the same? – mitchellJ Mar 23 '15 at 13:58
  • It seems you're implementing your own authentication, So have you employed/implemented `FilterAttribute`s too? Why you have ignored Interfaces like `IPrincipal`? Also seems you're using combination of WebForms and MVC! These make the problem more complex to solve. – Amirhossein Mehrvarzi Mar 25 '15 at 09:01
  • http://stackoverflow.com/questions/13343073/asp-net-mvc-4-cookie-disappears May help you since your deployment area differs in `machinekey` configuration while in local you can ignore such this options since it employs a single one in top level configuration file. – Amirhossein Mehrvarzi Mar 25 '15 at 09:17
  • CustomPrincipal is an IPrincipal. P.S. it's good practice to leave an explanation when you downrate a question. – mitchellJ Mar 25 '15 at 14:21
  • Silly question, but do you have any security bypasses written into a precompiler statement somewhere that you forgot about? Sometimes I forget that I do some logic that works because I'm working in debug then when I publish in release, it compiles different logic that I forgot to check. – Sinaesthetic Mar 27 '15 at 16:15
  • @Sinaesthetic Good thought but all I have are debug statements to redirect my SMTP traffic during notifications testing. – mitchellJ Mar 27 '15 at 17:09

2 Answers2

1

Most likely, you are attempting to retrieve HttpContext.User before it has been initialized. This behavior differs between IIS Classic (or the Visual Studio Web Server) and IIS Integrated pipeline modes, which would explain why you are seeing different behavior between the environments.

Explanation

HttpContext is part of an application's runtime state. In modern hosting environments (IIS integrated pipeline mode and OWIN), HttpContext is not populated until after the Application_Start method is complete. Any behavior that you have that requires HttpContext.User should not be executed until the Application_BeginRequest event or after.

Reference: Request is not available in this context

Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • But by this point the Application_Start should have already been triggered, correct? My controller factory is launched in the Application_Start and it is not until after authentication that the data is dropped. – mitchellJ Mar 24 '15 at 18:44
  • Well, you haven't posted where in the application you are getting the error, so this is a bit of a shot in the dark. But the symptoms fit. If using DI, are you accessing the HttpContext.User property in the call stack of the constructor of one of your services? Note that you must not do this. See [this answer](http://stackoverflow.com/questions/27052029/null-user-on-httpcontext-obtained-from-structuremap/27075242#27075242) for the solution. – NightOwl888 Mar 24 '15 at 19:36
  • Note that you can prove or disprove this theory by setting up your local environment to run in IIS integrated mode. If that causes the error to happen in your dev environment, then you can more easily track down the issue. – NightOwl888 Mar 24 '15 at 19:39
  • All the information I can find on the VS Dev Server says that it can't be switched to Integrated Pipeline. Do you know of a way to switch it? – mitchellJ Mar 24 '15 at 19:57
  • You would need to temporarily host it under local IIS in order to enable integrated mode (in the app pool). The VS Dev Server doesn't have that option. – NightOwl888 Mar 24 '15 at 20:06
1

It's not clear from your post since configuring the authentication depends on various settings in your project plus the configuration file. For example in Web.config file, there are several places to customize/configure the authentication like this one (the most important rule) that you haven't placed in your post:

<system.web>
   <authentication mode="" />
</system.web>

As you know, since the configuration system is based on a hierarchical system of management system that uses **.config* files, you should consider the defaults, perhaps by <remove/> or <add/> some parameters. The configuration files for IIS 7 and later are located in your %WinDir%\System32\Inetsrv\Config folder, and the primary configuration files are:

  • ApplicationHost.config - This configuration file stores the settings for all your Web sites and applications.
  • Administration.config - This configuration file stores the settings for IIS management. These settings include the list of management modules that are installed for the IIS Manager tool, as well as configuration settings for management modules.
  • Redirection.config - IIS 7 and later support the management of several IIS servers from a single, centralized configuration file. This configuration file contains the settings that indicate the location where the centralized configuration files are stored.

Note: Some settings can be delegated to Web.config files, which may override settings in the ApplicationHost.config file. In addition, settings that are not delegated cannot be added to Web.config files.

Tip: A default installation of IIS 7 does not contain Digest authentication, so adding the settings for Digest authentication to your ApplicationHost.config will have no effect or may cause errors until the Digest authentication module is installed.

You need to see both local and deployment configurations to meet your purpose. If you have trouble with integrated pipeline, see the following pages to take its advantages :

Update About SlidingExpiration : According to MSDN:

Sliding expiration resets the expiration time for a valid authentication cookie if a request is made and more than half of the timeout interval has elapsed.

If the cookie expires, the user must re-authenticate. Setting the SlidingExpiration property to false can improve the security of an application by limiting the time for which an authentication cookie is valid, based on the configured timeout value. So I think there's no need to use this as false. This means It will expire cache after time period at the time of activating cache if any request is not made during this time period. This type of expiration is useful when there are so many data to cache. So It will put those items in the cache which are frequently used in the application. So it will not going to use unnecessary memory.

Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70