'HttpContext.User NullReferenceException only on deployed server
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 NullReferenceException
s.
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; }
Solution 1:[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
Solution 2:[2]
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 :
- Debugging the IIS7 integrated pipeline with failed request tracing
- How to Take Advantage of the IIS 7.0 Integrated Pipeline
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Community |
Solution 2 |