UPDATE:
I solved this problem with just a few fairly simple changes, see my self-answer below.
ORIGINAL QUESTION:
I have an ASP.NET web app that uses both Windows authentication and Forms authentication. Forms
authentication is defined as the authentication mode
in Web.config (see below for excerpt). In IIS 7, at the web app (AKA virtual directory) level, anonymous authentication is disabled, and windows authentication is enabled.
In .NET 1.1 to .NET 4.0 and IIS6/7/7.5 after successfully authenticating via Windows auth, but before authenticating via Forms auth (creating the forms authentication ticket / cookie), Global.Application_AuthenticateRequest()
sees that Request.IsAuthenticated
is false
. And once Request.IsAuthenticated
becomes true
the System.Web.HttpContext.Current.User
is of type System.Security.Principal.GenericPrincipal
(and User.Identity
is System.Web.Security.FormsIdentity
)
This behavior changed after .NET 4.5 was installed on the IIS7 server. No changes were made to the Web.config file, and no changes were manually made to IIS. The only change I made was to install .NET 4.5. The behavior reverted back to 'normal' after uninstalling 4.5 and reinstalling 4.0.
The different behavior I noticed is that after successfully authenticating via Windows, but before authenticating via forms (the forms authentication ticket has not yet been created), Application_AuthenticateRequest
now shows that Request.IsAuthenticated
is true
.
Also, the System.Web.HttpContext.Current.User.Identity
is now System.Security.Principal.WindowsIdentity
(instead of FormsIdentity
).
Request.IsAuthenticated = true
?)I have been searching Msft docs for hours.. all their info about mixing Windows and Forms auth seems to be years out of date (2004-ish), and the details on changes to .NET 4.5 are rather sparse in this particular area.
Excerpt from web.config: (yes, default.aspx is intentional, I don't use login.aspx in this case, but it has been working fine for 5+ years and across all previous .net versions).
<authentication mode="Forms">
<forms name=".ASPXAUTH" protection="All" timeout="200" loginUrl="default.aspx" defaultUrl="~/default.aspx" />
</authentication>
Excerpt from Global.asax.cs:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (Request.IsAuthenticated)
{
// stuff for authenticated users
// prior to upgrading to .NET 4.5, this block was not hit until
// after the forms authentication ticket was created successfully
// (after validating user and password against app-specific database)
}
else
{
// stuff for unauthenticated users
// prior to upgrading to .NET 4.5, this block was hit
// AFTER windows auth passed but BEFORE forms auth passed
}
}
Re: Can somebody please explain why this is different?
I noticed a change in System.Web.Hosting.IIS7WorkerRequest.SynchronizeVariables() was made in 4.5. The difference is shown as below (source code is from reflector):
In 4.0, SynchronizeVariables() only synchronized the IPrincipal/IHttpUser if Windows authentication was enabled.
internal void SynchronizeVariables(HttpContext context)
{
...
if (context.IsChangeInUserPrincipal && WindowsAuthenticationModule.IsEnabled)
// the second condition checks if authentication.Mode == AuthenticationMode.Windows
{
context.SetPrincipalNoDemand(this.GetUserPrincipal(), false);
}
...
}
In 4.5, SynchronizeVariables() synchronizes the IPrincipal/IHttpUser if any authentication is enabled (AuthenticationConfig.Mode != AuthenticationMode.None)
[PermissionSet(SecurityAction.Assert, Unrestricted=true)]
internal void SynchronizeVariables(HttpContext context)
{
...
if (context.IsChangeInUserPrincipal && IsAuthenticationEnabled)
{
context.SetPrincipalNoDemand(this.GetUserPrincipal(), false);
}
...
}
private static bool IsAuthenticationEnabled
{
get
{
if (!s_AuthenticationChecked)
{
bool flag = AuthenticationConfig.Mode != AuthenticationMode.None;
s_AuthenticationEnabled = flag;
s_AuthenticationChecked = true;
}
return s_AuthenticationEnabled;
}
}
I suspected the above change is the root cause of the behavior change in authentication.
Before the change: ASP.NET does not sync up with IIS to get user's windows identity (IIS does make windows authenticaiton though). Because no authentication is done, ASP.NET can still do forms authentication.
After the change: ASP.NET syncs up with IIS to get the user's windows identity. Because context.Current.User is set, then ASP.NET would not do forms authentication.
UPDATE:
I solved this problem by implementing two-stage authentication (windows auth first (if enabled), then forms auth), and by avoiding using Request.IsAuthenticated
altogether.
I created a static property in one of my project's common libraries: Security.User.IsAuthenticated
and now use this where I was previously using Request.IsAuthenticated
. Now I have full control over what "is authenticated" means in my application. (Shoulda done this in the first place; as the years go by I routinely find myself wrapping existing .NET functionality like this to give me finer control!)
Sorry, can't reveal the exact details, but basically it involves checking some stuff from the current request's context (System.Web.HttpContext.Current
) that gets set when the forms authentication ticket is created upon successful login (whether via SSO or otherwise). Hope this helps somebody...
Here's the property I created. (The real work is done by the ProprietaryAuthenticationFunction(), which should be whatever your app needs to do to validate against database, LDAP, or whatever. Sorry, can't share that code with you because it would violate the terms of my contract, but most enterprise apps should already have their own proprietary authentication function anyway.)
/// <summary>
/// This only returns true when current request is authenticated via forms
/// authentication, meaning the user is logged into the proprietary web app
/// (whether by manual login with user/pass or by single sign-on) AND has
/// passed whatever authentication method is used by IIS.
/// </summary>
public static bool IsAuthenticated
{
get
{
bool isAuth =
System.Web.HttpContext.Current != null &&
System.Web.HttpContext.Current.Request != null &&
System.Web.HttpContext.Current.Application != null &&
System.Web.HttpContext.Current.Session != null &&
System.Web.HttpContext.Current.Request.IsAuthenticated &&
ProprietaryAuthenticationFunction(System.Web.HttpContext.Current.Application, System.Web.HttpContext.Current.Session);
return isAuth;
}
}
(Note that this particular web app makes heavy use of Session
, but if you don't care about Session
you could omit those parts. Please let me know if you see any problems / security holes / performance considerations or whatever, thanks!)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With