Monday, November 3, 2014

IIS7 Mixed Windows and Forms Authentication

I recently undertook a project with the following requirements:
- an ASP .NET website hosted on IIS 7.5
- Windows authentication for users logged in to the organisation's internal domain (single sign-on)
- forms (user/password) authentication for anonymous public internet users

While simple enough in theory, it seems that this scenario is officially unsupported by Microsoft.

With Google searches yielding a mix of results, mostly for a mix of IIS versions and a variety of approaches, I was able to finally arrive at a solution. In the process, I learnt a lot about the ASP .NET/IIS authentication system.
Because the most relevant articles on StackOverflow come to the conclusion that this kind of Mixed Mode Authentication is not possible, I thought I'd write this post.

Firstly, I'd like to go over some of the principles that I found applied to this process.
Then I'll sketch the solution I used.
If you'd like to save time, I also offer a complete C# skeleton solution for download.

IIS Versions and Settings

Firstly, the authentication model fundamentally changed at IIS 7. This solution deals only with IIS 7+ 'Integrated Pipeline' mode authentication.
Earlier IIS versions or IIS 7+ running in 'classic' pipeline mode work in a different way and this post is not relevant to that situation.

In IIS 7 or 7.5, three authentication types need to be selected: Windows, Forms and Anonymous. You'll get a warning from IIS about mixing challenge and redirect authentication types, but you can ignore it.

Forms Authentication Overview

Let's cover a few characteristics of Forms Authentication:
- authentication is recorded and maintained by use of a cookie (except in very rare case not dealt with here)
- Forms Authentication safeguards access to the site: on initial request by an unauthenticated user, a redirection occurs to the 'login' page specified in the web.config, with the original request URL appended to the login page URL in the querystring (this is why it's a 'redirect' authentication type)
- if Forms Authentication is enabled, successful Windows Authentication does not prevent the forms-auth redirect from occurring: it is necessary for code to create a valid forms-authentication cookie before the site can be accessed
- specific folders or pages of the web site can be opened up to anonymous users, and can then be accessed without triggering the redirect mechanism

In the main <system .web=""> section of the web.config, the <authorization> section specifies the access for the whole site:
<authorization>
  <deny users="?">
    <allow users="*">
    </allow>
  </deny>
</authorization>
'?' represents anonymous users, and '*' all users (authenticated and non-authenticated/anonymous), so denying all anonymous users and then allowing all users is equivalent to allowing only authenticated users.

Then outside the main <system .web=""> section (ie. in the top level <configuration> section), we add authorization settings for specific pages or folders
<location path="css">
  <system .web="">
    <authorization>
      <allow users="?">
      </allow>
    </authorization>
  </system>
</location>
<location path="AccountRecover.aspx">
  <system .web="">
    <authorization>
      <allow users="?">
      </allow>
    </authorization>
  </system>
</location>
The first gives anonymous access to the 'css' subfolder of the root folder, and the second gives anonymous access to the 'AccountRecover.aspx' page.
It is common practice in ASP .NET to give anonymous access to 'content' folders such as image, css and js folders since a request to the Login page will often refer to images or css files necessary to deliver the look and feel of the site prior to a user logging in. There is rarely any security issue giving anonymous users access to these folders.
The AccountRecover.aspx page needs to be given anonymous access because, by definition, the user won't be logged in if they have forgotten their details and wish to recover their account. There are a few other pages that need this access also.

Windows Authentication Overview

- Windows authentication, if selected by itself, normally triggers IIS itself (not ASP .NET) to issue a HTTP 401 challenge on first page request for an unauthenticated user
- if Anonymous authentication is enabled, then authentication defaults to anonymous on first page request, and no Windows auth 401 challenge occurs. However, the web server may manually issue a 401 challenge at any point after this
- The 401 challenge is handled by the browser typically like this (different browsers can vary):
  (a) if the user is already logged in to a Windows domain, then the challenge is usually answered invisibly by the browser, providing the logged on credentials.
  (b) if the user is not logged on to a Windows domain, then a user name and password window will be displayed by the browser, inviting the user to log on to such a domain
  (c) the logon credentials are returned to the web server and if they pass authentication, the user is now logged in. If they fail then the browser displays the 401 Not Authorized error. The user can try again by navigating 'back'.

Authentication Settings in Asp .NET

A range of settings are mentioned by the various articles on authentication:

System.Security.Principal.WindowsIdentity.GetCurrent().Name

This is the identity of the IIS worker process.
It may be the default system/IIS process, or a service or user account if the application pool has been assigned to a particular account, or it may even be a dynamic user account if Identity Impersonation has been turned on.
However this is an IIS artifact, and nothing to do with us in the current scenario, so we will ignore it.

Request.ServerVariables["LOGON_USER"]

This is cited by various articles out there as the logged on Windows user. Perhaps this was relevant before IIS 7, but now it appears to echo Page.User.Identity.Name as below

Page.User.Identity.Name

If logged in via Forms Authentication, this contains the username used to log in.
If authenticated by Windows, it contains the name (including the domain) of the Windows identity used to answer the 401 challenge.


Issues Mixing Windows and Forms Authentication

There appears to be only a single successful strategy to mix the two types of authentication, and that is to use forms authentication as the basis for the login system, but also allow the user to trigger and respond to a 401 challenge, programmatically creating a forms authentication ticket if successful.

The issues with this are the following:

- Page.User.Identity.Name returns different values (as above) depending on what type of authentication is active. If Windows authentication is active, it can't be turned off except by closing and reopening the browser. Forms authentication is only active if Windows authentication is not.
For this reason every page in the application should be able to identify a user given either their Forms Authenticated username, or by their Windows credentials.
In particular, it should not be possible for one user to have a Forms username that is the same as another user's Windows credentials. Luckily, Windows credentials appear to always contain a domain and a backslash. The presence of a backslash in the username is a reasonable test for the type of authentication that is active.

- Windows authentication often seems to drop out mid-session, falling back to into forms mode using the generated forms auth cookie. I'm just warning about this; it's not a problem - unless relying on some aspect of one of the the types of authentication.

- the pre-built ASP .NET forms authentication and authorization management controls work fine for forms authenticated users, but choke for Windows authenticated users because they try to use the authenticated username (which is the Windows username) to look up the forms-based user records. It is possible to find the equivalent forms user name and programmatically feed it in to these controls to emulate forms auth when using Windows auth. For example, with the <asp:changepassword> control, the 'UserName' property allows us to do this

Putting It All Together

Given that most users requiring Windows authentication will already be logged in to their domain, the most seamless authentication result would be if we could silently test for successful Windows authentication immediately after the Forms Authentication redirection to the Login page, and if successful, create the Forms Authentication ticket and proceed to the originally requested URL.

We can trigger immediate Windows Authentication by sending the 401 challenge on first entry to the Login page, but the problem lies in what occurs if the Windows login attempt fails.
Remember that once the 401 challenge has been sent, things proceed smoothly if credentials are accepted, but if they fail the browser will bring up a 401 'Not Authenticated' error for the page where the attempt failed and block further access to the site. The user could no longer access the Login page to attempt forms-based user/password login.

Give this problem, the best compromise is to leave the Login page as a purely forms-based page, and offer a button to trigger windows-based authentication only for those who want it. If windows authentication fails, clicking 'back' will go back to the login page, or if totally blocked, the browser can be closed and re-opened.

Another logical approach would be to construct two separate entry pages, one for windows authentication users and one for forms authentication users. This was tested, but does not operate well within the redirect based authentication paradigm, and I could not reliably prevent the Windows Authentication URL from being redirected to the Forms redirect URL under some conditions, even if the page was added to the Forms Authentication anonymous access list as shown earlier.
This option was therefore discarded.

In summary, the key points are:
- the Windows login credentials for a user account should be stored as an additional column in the ASP .NET Membership provider user record (thus linking it to a Forms based user)
- the login screen should be forms based, and contain a button to trigger Windows login
- the Windows login button should respond with an HTTP 401 response challenge
- the login page and all other applications should be able to identify a Forms user given either their Forms Authenticated username, or by their Windows credentials. It must be ensured that Forms and Windows usernames are distinct and unique.
- where a user has successfully Windows Authenticated, but the credentials are not recognised, a separate linking page may be created to allow the user to log in using their Forms based username and password, and for the Windows credential to be saved against the Forms user database record.

final note!
I found this StackOverflow question.
Has anyone tried this approach? I note above that I couldn't get the two-pages approach to work because of the forms redirect, however I didn't know about the ability to specify authentication mode per-page. That could be an alternative.

Sample Application

At arrow-of-time.com, I am offering a complete Visual Studio C# skeleton solution for download. This solution contains the ASP .NET pages and classes to achieve all of the discussed behaviour, and detailed deployment instructions.


2 comments:

  1. Here's a solution I found that works great for me. It sends the 401 challenge immediately, using Application_EndRequest. Be sure to check out my reply at the bottom, which modifies the 401 response to automatically redirect to the forms login page in case Windows authentication fails (this should be rare, because it only attempts Windows authentication for Windows clients running on the local network):

    http://world.episerver.com/blogs/Dan-Matthews/Dates/2014/8/Mixing-Forms-and-Windows-Authentication/

    ReplyDelete