HTTP Basic Authentication against Non-Windows Accounts in IIS/ASP.NET (Part 2 – The HTTP Module)

An HTTP module is one of the main extensibility points in ASP.NET/IIS7. Modules subscribe to notifications of certain stages in the HTTP request/response processing. Inside of the event handlers you can then inject your custom code and logic.

To allow some flexibility in the module’s logic there is also a configuration section where the following settings can be configured:

  • enabled yes/no
  • realm name
  • the membership provider that should be used to validate credentials (default means use the default provider, otherwise provide the name of the provider)
  • require SSL
  • should credential caching be enabled – if yes how long (I mimick here the IIS built-in token caching and default to 15 minutes)

The following configuration section reflects these options:

<!-- custom basic authentication configuration -->
<customBasicAuthentication enabled="true"
                           realm="leastprivilege"
                           providerName="default"
                           cachingEnabled="true"
                           cachingDuration="15"
                           requireSSL="true" />

I skip the details on how to create configuration sections. There are also some differences when it comes to support IIS 6 and 7. In IIS6 this section would go to <system.web /> whereas in IIS 7 <system.webServer /> would be used.

The next step is to write a class that implements the IHttpModule interface and its Init method. Inside of Init the module subscribes to two events in the pipeline: AuthenticateRequest and EndRequest:

// event registration
public void Init(HttpApplication context)
{
    context.AuthenticateRequest += OnEnter;
    context.EndRequest += OnLeave;
}

What’s happening in AuthenticateRequest
Here the module hast to distinguish between two states:

  • No Authorization header present.
    This means the request is anonymous. The module now has to check if anoynmous access is allowed – if this is the case, the module does nothing and passes the request on. Right after the AuthenticateRequest stage runs the AuthorizeRequest stage. If one of the subscribed modules determines that authentication is needed for the requested resource, it will put a 401 on the status code and jump directly to EndRequest. Here our EndRequest logic kicks in (more on that in a second).
  • Authorization header is present.
    This means the user tries to authenticate. In this case the module extracts the credentials from the header and passes them on to the configured membership provider (you could of course plugin whatever credentials verification logic you want).

Checking if anonymous access is configured in IIS is only included in the IIS7 version of the module.

The corresponding code looks like this:

void OnEnter(object sender, EventArgs e)
{
    HttpContext context = HttpContext.Current;

    // check if module is enabled
    if (!Configuration.Enabled)
        return;

    // check if SSL is required and enabled
    if (Configuration.RequireSSL && !context.Request.IsSecureConnection)
    {
        throw new HttpException(403, "SSL required for Basic Authentication");
    }

    // try to authenticate user - otherwise set status code and end request
    if (IsHeaderPresent)
    {
        if (!AuthenticateUser())
        {
            DenyAccess();
        }
    }
    else
    {
        // if anonymous requests are not allowed - end the request
        if (!IsAnonymousAllowed)
        {
            DenyAccess();
        }
    }
}

The DenyAccess method sets the 401 status code and ends the current request. Ending a request means that processing jumps directly to EndRequest. This is were the OnLeave handler kicks in.

private static void DenyAccess()
{
    HttpContext context = HttpContext.Current;

    context.Response.StatusCode = 401;
    context.Response.End();
}

 

What’s happening in EndRequest?
The logic in EndRequest is simple. It checks if a 401 status code is set (by the module itself or maybe by an authorization module or a page) and if that is the case, sends the necessary HTTP headers back to start the authentication handshake.

void OnLeave(object sender, EventArgs e)
{
    // check if module is enabled
    if (Configuration.Enabled)
    {
        if (HttpContext.Current.Response.StatusCode == 401)
        {
            SendAuthenticationHeader();
        }
    }
}

 

Authenticating the user
The authentication logic uses a membership provider under the covers and additionally implements caching of the client “token”.

private bool AuthenticateUser()
{
    string username = "", password = "";
    string authHeader = HttpContext.Current.Request.Headers["Authorization"];

    if (authHeader != null && authHeader.StartsWith("Basic"))
    {
        // extract credentials from header
        string[] credentials = ExtractCredentials(authHeader);
        username = credentials[0];
        password = credentials[1];

        if (IsCredentialCached(username, password))
        {
            SetPrincipal(username);
            return true;
        }
        else if (Provider.ValidateUser(username, password))
        {
            if (Configuration.CachingEnabled)
            {
                CacheCredential(username, password);
            }

            SetPrincipal(username);
            new AuthenticationSuccessEvent(this, username).Raise();

            return true;
        }
    }

    new AuthenticationFailureEvent(this, username).Raise();
    return false;
}

The AuthenticationFailureEvent/AuthenticationSuccessEvent classes are Health Monitoring WebEvents (System.Web.Management). They push tracing information out to configured listeners. The nice thing is that in IIS7 you can forward these WebEvents to the FREB tracing infrastructure.

 

Caching
Once the Authorization header is initially set, it gets re-sent on every subsequent request. This means that the authentication logic kicks in every time. If you would do a (remote) database roundtrip to your credential store on every authentication request, this could impact performance.

To improve this situation, you can implement a simple caching scheme:

  • Once the use is authenticated, cache a unique identifier for that username/password pair
  • When re-authentication happens, re-create that unique identifier from the supplied credentials and check if this id is in the cache

The two methods IsCredentialCached and CacheCredential implement this logic:

// caches a credential identifier
private void CacheCredential(string username, string password)
{
    string identifier = GetIdentifier(username, password);

    try
    {
        HttpContext.Current.Cache.Add(
            identifier,
            "ok",
            null,
            DateTime.Now.AddMinutes(Configuration.CachingDuration),
            Cache.NoSlidingExpiration,
            CacheItemPriority.Normal,
            null);

        new CredentialCacheAddEvent(this, username).Raise();
    }
    catch (Exception ex)
    {
        new CredentialCacheAddErrorEvent(this, username, ex).Raise();
    }
}// checks if the credential has been cached already
private bool IsCredentialCached(string username, string password)
{
    if (!Configuration.CachingEnabled)
        return false;

    string identifier = GetIdentifier(username, password);
    bool cacheHit = (HttpContext.Current.Cache[identifier] != null);

    if (cacheHit)
    {
        new CredentialCacheHitEvent(this, username).Raise();
    }
    else
    {
        new CredentialCacheMissEvent(this, username).Raise();
    }

    return cacheHit;
}

Creating an identifier
To get rid of any clear text secrets in your cache, simply hash the username and password. If you are paranoid, add a salt to it – but if an attacker is so close to your RAM to read the cache, you are in trouble anyways ;)

// create a string identifier for the cache key
// format: prefix + Base64(Hash(username+password))
private string GetIdentifier(string username, string password)
{
    // use default hash algorithm configured on this machine via CryptoMappings
    // usually SHA1CryptoServiceProvider
    HashAlgorithm hash = HashAlgorithm.Create();

    string identifier = username + password;
    byte[] identifierBytes = Encoding.UTF8.GetBytes(identifier);
    byte[] identifierHash = hash.ComputeHash(identifierBytes);

    return _cachePrefix + Convert.ToBase64String(identifierHash);
}

Setting the authentication header
The last missing piece is the code that sets the Authenticate header and kicks off the authentication handshake.

// send header to start Basic Authentication handshake
private void SendAuthenticationHeader()
{
    HttpContext context = HttpContext.Current;

    context.Response.StatusCode = 401;
    context.Response.AddHeader(
        "WWW-Authenticate",
        String.Format("Basic realm="{0}"", Configuration.Realm));
}

 

OK – that’s it. You can download the full code here. There is also a test webapp included. In the next post I will detail how to configure IIS/ASP.NET to use the authentication module.

This entry was posted in ASP.NET, WCF. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s