Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing Windows and Forms authentication in .NET 4.5: how to keep Request.IsAuthenticated = false until after forms authentication ticket is created?

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).

  1. Can somebody please explain why this is different?
  2. Is there a configuration option (like web.config change or IIS setting) that I can use to force it to work the 4.0 way? (so that windows auth does not trump forms auth with respect to setting 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
        }
    }
like image 710
nothingisnecessary Avatar asked Dec 31 '13 20:12

nothingisnecessary


2 Answers

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.

like image 166
X-Mao Avatar answered Oct 23 '22 05:10

X-Mao


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!)

like image 2
nothingisnecessary Avatar answered Oct 23 '22 03:10

nothingisnecessary