Thursday 24 August 2006

'Mashing up' Windows AND Forms Authentication

Jeff

I had a classic requirement that a website must automatically log in users that have authenticated against its local domain controller (windows authentication). Any users who have not authenticated with its DC will need to login using a web based login form, which will then authenticate them against the DC using the ActiveDirectoryMembershipProvider.

I have used these resources to tackle this requirement:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/MixedSecurity.asp - Paul Wilsons msdn article titled "Mixing Forms and Windows Security in ASP.NET"

http://aspadvice.com/blogs/rjdudley/archive/2005/03/10/2562.aspx - Richard Dudley's blog about how he modified Pauls method to stop the browser popup for credentials for remote 'internet' users.

I'm just going to walk through my solution for my own reference and for anyone else with this requirement.

1. Make sure the whole website has the 'Enable Anonymous Access' checkbox ticked under IIS->Website->Properties->Directory Security->Edit->Enable Anonymous Access.
Note: The Integrated Windows authentication check box, under the Authenticated access, may also be selected as this is required to debug in VS.
2. Create both WinLogin.aspx and FormsLogin.aspx pages.
3. Create a Redirect401.htm file.
4. In the web.config file I have the following:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0%22>
...
<location path="FormsLogin.aspx">
<system.web>
<authorization>
<allow users="?,*" />
</authorization>
</system.web>
</location>

<location path="WinLogin.aspx">
<system.web>
<authorization>
<allow users="?,*" />
</authorization>
</system.web>
</location>

<appSettings>
...
<add key="LanIPMask" value="192.168.\d{1,3}\.\d{1,3}"/>
...
</appSettings>


...
<system.web>
...
<authentication mode="Forms">
<forms name=".ADAuthCookie"
slidingExpiration="true" loginUrl="FormsLogin.aspx"/>
</authentication>
...
</system.web>
</configuration>

5. The FormsLogin just has the ASP.NET Login control and in the code behind of the I have the following:

protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

// Is this a postback?
if (!Page.IsPostBack)
{
// NO - this is not a post back.

// Try to authenticate the user via windows Auth.
AttemptWindowsAuth();

// The user must authenticate using Forms.
}
}

private void AttemptWindowsAuth()
{
// Is the user not using internet explorer?
if (!Request.Browser.IsBrowser("IE"))
{
// NO - the user is not using IE and therefore can not perform windows authentication, don't redirect them.
return;
}

// Is the user on a mobile device?
if (Request.Browser.IsMobileDevice)
{
// YES - the user is on a mobile device, don't use windows auth.
return;
}

// Has the user already had a failed login?
if (Request.QueryString["failedlogin"] != null)
{
// YES - the user has already had a failed login, don't redirect them again.
return;
}
// Is the user on the local Lan?
if (Regex.IsMatch(this.Request.UserHostAddress, ConfigurationManager.AppSettings["LanIPMask"]))
{
// YES - the user is on the local lan so redirect them to the windows page for windows Auth.
RedirectToWinAuth();
}

// Is the user on the local server?
if (this.Request.UserHostAddress.Equals("127.0.0.1") this.Request.UserHostName.ToLower().Equals("localhost"))
{
// YES - the user is on the local server so redirect them to the windows page for windows Auth.
RedirectToWinAuth();
}
}

private void RedirectToWinAuth()
{
// Transfere to the windows login page.
Response.Redirect("WinLogin.aspx?" + Request.QueryString.ToString(), true);
}

5. In IIS make sure the WinLogin.aspx does NOT allow anonymous access and only uses Integrated Windows authentication to authentic access. This can be set by navigating to IIS->Website->WinLogin.aspx->Properties->Directory Security->Edit
6. Whilst you are in IIS navigate to IIS->Website->WinLogin.aspx->Properties->Custom Errors and change all the 401 errors to point to your Redirect401.htm file you created earlier.
7. The winLogin.aspx is an empty page and in the codebehind has the following:

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
int start = this.Request.ServerVariables["LOGON_USER"].LastIndexOf('\\');
string userName = this.Request.ServerVariables["LOGON_USER"].Substring(start + 1);
FormsAuthentication.RedirectFromLoginPage(userName, false);
}

8. The Redirect401.htm has the following html:

<html xmlns="http://www.w3.org/1999/xhtml%22>
<head>
<title>Redirect 401</title>
<script type="text/javascript" language="javascript">
window.location = "FormsLogin.aspx?failedlogin=1"
</script>
</head>
<body>
<p>
If you are not automatically redirected please click <a href="FormsLogin.aspx?failedlogin=1">here</a>
</p>
</body>
</html>

So the users always hits the Formslogin page for authentication, it will check to see if their IP address matched the RegEx expression in the web config (which is the mask for local IP addresses) if it does then they are redirected to the Windows login page which will cause IIS to authenticate them using windows authentication. If this is successful they will be redirected via formsauthentication, giving them a forms authentication ticket :-) They are now free to move around the site.
If they are not in the local IP range they will be shown the forms login page for them to enter their details and use the ActiveDirectoryMembershipProvider to authenticate them against active directory.
If the user has just 'plugged into' the local domain and has received an IP address via the DHCP, when they visit the site they will be pushed to the windows authentication page and as they have not been authenticated by the DC they will be prompted with the browsers credential request box. If they cancel this or enter an invalid username and password combination a 401 error will be raised and handled by are custom page which will redirect them back to the FormsLogin page. The only way for them to gain access to the system is to enter a valid username and password that is stored in Active Directory.
I also use the SQLRolesProvider within this web application and it works fine with this solution.

No comments: