The goal of this post is to document the “not-quite-practical” possibility of replacing Forms Authentication Module with a “401 Challenge”-Authentication-Module but still be able to use a custom membership provider.
The clarification could possibly dispel a common confusion – in ASP.NET the 401 Challenge authentication is often confused with Windows authentication scheme. The problem stems from the fact that the authentication module and the membership provider are two distinct responsibilities:
- your authentication could be “401 Challenge” based or cookie based (or even anything-else-based)
- your users credentials can be validated against AD or against a custom user store
Now, if you think about it, 4 combinations sound to make sense:
Membership Authentication | Windows membership | Custom membership |
302 Redirect (cookie) | ActiveDirectoryMembershipProvider <authentication mode="Forms"> | Any custom membership provider <authentication mode="Forms"> |
401 Challenge | Windows authentication <authentication mode="Windows"> | ??? |
You can have cookie based (forms) authentication with any membership provider, including the built-in ActiveDirectoryMembershipProvider. You can have “401 Challenge” based authentication with Windows accounts.
But what about “401 Challenge” based authentiaction with a custom membersip provider? ASP.NET has no built-in solution for this. You can have 401 Challenge-based authentication only for Windows accounts, in windows authentication mode.
This is why some people think that 401 Challenge basic authentication is only possible with Windows accounts.
Does it make sense?
Good question. There are many drawbacks of the 401 Challenge authentication, just to name a few:
- the login window is not customizable, it is built into your web browser
- there is no easy way to signal “wrong credendials” (a new 401 is returned)
There are also pros:
- 401 Challenge can be used in “active” scenario, where a client (ajax? webapi client?) can carry credentials in the very first request, without the need to redirect-with-a-cookie and then carry-cookie-with-each-request. In fact, the authentication header is automatically handled by all web browsers.
How to do it?
The code below is based mostly on the WebApi authentication module by Mike Wasson from his entry on WebApi. Minor modifications introduces authorization handling (the authentication is required only when authorization fails) and a call to a membership provider.
/// <summary>
/// Based on http://www.asp.net/web-api/overview/security/basic-authentication
/// </summary>
public class BasicAuthHttpModule : IHttpModule
{
public void Init( HttpApplication context )
{
// Register event handlers
context.AuthenticateRequest += OnApplicationAuthenticateRequest;
context.EndRequest += OnApplicationEndRequest;
}
private static void SetPrincipal( IPrincipal principal )
{
Thread.CurrentPrincipal = principal;
if ( HttpContext.Current != null )
{
HttpContext.Current.User = principal;
}
}
private static bool AuthenticateUser( string credentials )
{
bool validated = false;
try
{
var encoding = Encoding.GetEncoding( "iso-8859-1" );
credentials = encoding.GetString( Convert.FromBase64String( credentials ) );
int separator = credentials.IndexOf( ':' );
string name = credentials.Substring( 0, separator );
string password = credentials.Substring( separator + 1 );
if ( Membership.ValidateUser( name, password ) )
{
var identity = new GenericIdentity( name );
SetPrincipal( new GenericPrincipal( identity, null ) );
}
}
catch ( FormatException )
{
// Credentials were not formatted correctly.
validated = false;
}
return validated;
}
private static void OnApplicationAuthenticateRequest( object sender, EventArgs e )
{
var request = HttpContext.Current.Request;
var user = Thread.CurrentPrincipal;
if ( !UrlAuthorizationModule.CheckUrlAccessForPrincipal(
request.Path, user, request.HttpMethod ) )
{
var authHeader = request.Headers["Authorization"];
if ( authHeader != null )
{
var authHeaderVal = AuthenticationHeaderValue.Parse( authHeader );
// RFC 2617 sec 1.2, "scheme" name is case-insensitive
if ( authHeaderVal.Scheme.Equals( "basic",
StringComparison.OrdinalIgnoreCase ) &&
authHeaderVal.Parameter != null )
{
AuthenticateUser( authHeaderVal.Parameter );
}
}
}
}
private static void OnApplicationEndRequest( object sender, EventArgs e )
{
var response = HttpContext.Current.Response;
if ( response.StatusCode == 401 )
{
response.Headers.Add( "WWW-Authenticate",
string.Format( "Basic realm=\"{0}\"",
HttpContext.Current.Request.Url.Host ) );
}
}
public void Dispose()
{
}
}
How to configure it?
There are two steps to configure the module. First, you register it for the processing pipeline:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="BasicAuthModule" type="The.Namespace.Here.BasicAuthHttpModule"/>
</modules>
</system.webServer>
The other important moment of the configuration is turning off all other 401 Challenge handling modules in the application configuration in IIS:
As you can see, the only active authentication method is “Anonymous” (Włączone = Enabled, Wyłączone = Disabled).
What happens if you don’t turn off other 401 Challenge handling methods? Well, the pipeline just uses them and since all built in modules tie 401 Challenge to Windows authentication, the custom membership provider will not even be fired as the windows authentication will most probably reject provided credentials.
What now
You can test the implementation with an http debugger to see how 401 is returned, what browser does when it sees the status code and what goes to the server with the next request.
The basic 401 Challenge authentication scheme is just one of possible 401 Challenge flows. Other possibilities are Digest, Ntlm are Negotiate flows. Google for more details.