True ASP.NET Digest Authentication With Database
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version
Peter Bromberg

While doing some study on security and authentication mechanisms in ASP.NET, I was scrounging the Web for some good examples of HttpModules for doing BASIC authentication. I came across a remarkable piece of work by Greg Reinacker, who had taken the time to dissect RFC 2617 and succeeded in wrapping up Digest Authentication into an HTTP Module, complete with its own little XML document holding users, passwords and roles. You can find Greg's original piece here. I fiddled with this and although I did get his Webservice sample (which had a copy of the compiled HttpModule already in its /bin folder) to work, I kept getting errors when I tried to compile a new module from the source code he had published. Finally after going over his instructions carefully and making sure that I still hadn't done something wrong, I realized that the published source may not have been updated even though the fixed compiled DLL had been. So I just kept working on it and tracing through his code and finally got everything working as advertised. Kudos to Reinacker for putting out a tremendous piece of work and even more so for being willing to share it --others are simply selling this kind of stuff. Digest is really superior to other forms of authentication because it looks "professional", the browser re-sends the authenticated credentials automatically, and nothing has to go over the wire in the clear. It's not your username and password, its a Digest, which is a hash of a number of different items. I've seen several examples of using HttpModules for authentication. One person's presentation expected you to have the username and password on the querystring! I don't know what they were thinking...



While I was on this track I decided it would be useful to go ahead and wire up this puppy to use SQL Server instead of an XML file. And, for those of you who still feel "C-Sharp Challenged", I even went and wrote up a second version of it in VB.NET (with the aid of Lutz Roeder's Reflector).

Http Modules are .NET classes that implement System.Web.IHttpModule. By plugging your HttpModule into the Http pipeline, you can gain access to the various Request events, have your module invoked by the Runtime, and be able to play with or manipulate the Request. This interface exposes only two methods, Init(), which registers the Module's event handlers, and Dispose(), which allows us to perform any necessary cleanup before we go bye-bye and the Garbage Man comes to pick us up. You can register an HTTPModule for any of the following events raised by the HttpApplication instance:

AcquireRequestState: Raised when runtime is about to acquire the current HTTP Request.
AuthenticateRequest: Raised when ASP.NET is ready to authenticate the user issuing the request.
AuthorizeRequest: Same with authorization for resources.
BeginRequest: Raised when the Runtime receives a new HTTP Request.
Disposed: Raised when ASP.NET completes processing of an HTTP Request.
EndRequest: Raised immediately before the sending of content to the client.
Error: Raised when there is an unhandled exception during HTTP processing.
PreRequestHandlerExecute: Raised just before execution of an HTTP Handler.
PostRequestHandlerExecute: Raised just after HTTPHandler finishes executing.
PreSendRequestContent: Raised just before ASP.NET sends Response content to the client.
PreSendRequestHeaders: Raised just before headers are send to the browser.
ReleaseRequestState: Raised after ASP.NET completes executing all Request Handlers.
ResolveRequestCache: Raised to determine if the Request can be fulfilled from the Output Cache.
UpdateRequestCache: Raised when ASP.NET has finished processing and the output contents are ready to be added to the Cache.

In addition to these events, there are the "stock" Application and Session events which you can hook from within Global.asax. Obviously, it can be seen that there is a rich HTTP Pipeline event model here that we bloodthirsty developers can hook our programming teeth into. This particular HttpModule hooks both AuthenticateRequest and EndRequest, as you will soon see.

Module Registration

HttpModules are registered into the Http Pipeline in machine.config or in web.config, like so:


<httpModules>
<add name="DigestAuthenticationModule"
type="System.Web.Security.DigestAuthenticationModule,DigestAuthMod" />
</httpModules>

</system.web>

Now, let's take a look at some code. I've left almost all of Greg's work intact, except for the Reflector decompiler's mangling of variables and changing the namespace to the one that would be expected for this type of Authentication Module, and of course, my new code for handling Authentication and the return of roles via SQL Server, which is a little more robust scheme:

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections
Imports System.Collections.Specialized
Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient
Imports System.Security.Cryptography
Imports System.Security.Principal
Imports System.Text
Imports System.Web

Namespace System.Web.Security
    Public Class DigestAuthenticationModule
        Implements IHttpModule

        Public Overridable Sub Dispose() Implements IHttpModule.Dispose
        End Sub

        Public Overridable Sub Init(ByVal application As HttpApplication) Implements IHttpModule.Init
            AddHandler application.AuthenticateRequest, AddressOf Me.OnAuthenticateRequest
            AddHandler application.EndRequest, AddressOf Me.OnEndRequest
        End Sub

        Public Sub OnAuthenticateRequest(ByVal source As Object, ByVal eventArgs As EventArgs)
            Dim httpApplication As httpApplication = CType(source, httpApplication)
            Dim str1 As String = httpApplication.Request.Headers("Authorization")
            If Not str1 Is Nothing AndAlso str1.Length <> 0 Then
                str1 = str1.Trim()
                If str1.IndexOf("Digest", 0) = 0 Then
                    Dim i As Integer

                    str1 = str1.Substring(7)
                    Dim listDictionary As listDictionary = New listDictionary()
                    Dim strs1 As String() = str1.Split(New Char() {","c})
                    Dim str2 As String = String.Empty
                    For i = 0 To CInt(strs1.Length) - 1
                        Dim strs2 As String() = strs1(i).Split(New Char() {"="c}, 2)
                        str2 = strs2(0).Trim(New Char() {" "c, """"c})
                        Dim str3 As String = strs2(1).Trim(New Char() {" "c, """"c})
                        listDictionary.Add(str2, str3)
                    Next i
                    Dim str4 As String = CType(listDictionary("username"), String)
                    Dim str5 As String = ""
                    Dim strs3 As String()
                    If Not GetPasswordAndRoles(httpApplication, str4, str5, strs3) Then
                        DenyAccess(httpApplication)
                    Else
                        Dim str6 As String
                        Dim str7 As String = 
ConfigurationSettings.AppSettings("System.Web.Security.DigestAuthenticationModule_Realm") Dim str8 As String = String.Format("{0}:{1}:{2}", _
CType(listDictionary("username"), String), str7, str5) Dim str9 As String = GetMD5HashBinHex(str8) Dim str10 As String =
String.Format("{0}:{1}", httpApplication.Request.HttpMethod, _
CType(listDictionary("uri"), String)) Dim str11 As String = GetMD5HashBinHex(str10) If Not listDictionary("qop") Is Nothing Then str6 = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
New Object() {str9, CType(listDictionary("nonce"), String),
CType(listDictionary("nc"), String), CType(listDictionary("cnonce"), String),
CType(listDictionary("qop"), String), str11}) Else str6 = String.Format("{0}:{1}:{2}", str9, _
CType(listDictionary("nonce"), String), str11) End If Dim str12 As String = GetMD5HashBinHex(str6) Dim flag As Boolean = IsValidNonce(CType(listDictionary("nonce"), String)) = False httpApplication.Context.Items("staleNonce") = flag If CType(listDictionary("response"), String) = str12 AndAlso Not flag Then httpApplication.Context.User =
New
GenericPrincipal(New GenericIdentity(str4, "YourType"), strs3) Else DenyAccess(httpApplication) End If End If End If End If End Sub Public Sub OnEndRequest(ByVal source As Object, ByVal eventArgs As EventArgs) Dim httpApplication As httpApplication = CType(source, httpApplication) If httpApplication.Response.StatusCode = 401 Then Dim str1 As String =
ConfigurationSettings.AppSettings("System.Web.Security.DigestAuthenticationModule_Realm") Dim str2 As String = GetCurrentNonce() Dim flag As Boolean = False Dim local As Object = httpApplication.Context.Items("staleNonce") If Not local Is Nothing Then flag = CBool(local) End If Dim stringBuilder As stringBuilder = New stringBuilder("Digest") stringBuilder.Append(" realm=""") stringBuilder.Append(str1) stringBuilder.Append("""") stringBuilder.Append(", nonce=""") stringBuilder.Append(str2) stringBuilder.Append("""") stringBuilder.Append(", opaque=""0000000000000000""") stringBuilder.Append(", stale=") stringBuilder.Append(IIf(flag = True, "true", "false")) stringBuilder.Append(", algorithm=MD5") stringBuilder.Append(", qop=""auth""") httpApplication.Response.AppendHeader("WWW-Authenticate", stringBuilder.ToString()) httpApplication.Response.StatusCode = 401 End If End Sub Private Sub DenyAccess(ByVal app As HttpApplication) app.Response.StatusCode = 401 app.Response.StatusDescription = "Access Denied" app.Response.Write("401 Access Denied") app.CompleteRequest() End Sub Private Function GetMD5HashBinHex(ByVal val As String) As String Dim i As Integer Dim encoding As encoding = New ASCIIEncoding() Dim bs As Byte() = New _
MD5CryptoServiceProvider().ComputeHash(encoding.GetBytes(val)) Dim str1 As String = "" For i = 0 To 15 str1 = String.Concat(str1, String.Format("{0:x02}", bs(i))) Next i Return str1 End Function Protected Overridable Function GetCurrentNonce() As String Dim nonceTime As DateTime = DateTime.Now.AddMinutes(1) Dim expireStr As String = nonceTime.ToString("G") Dim enc As Encoding = New ASCIIEncoding() Dim expireBytes As Byte() = enc.GetBytes(expireStr) Dim nonce As String = Convert.ToBase64String(expireBytes) ' nonce can't end in '=', so trim them from the end Dim theChar(0) As Char theChar(0) = Convert.ToChar("=") nonce = nonce.TrimEnd(theChar) Return nonce End Function Protected Overridable Function IsValidNonce(ByVal nonce As String) As Boolean Dim dateTime As dateTime Dim i As Integer = nonce.Length Mod 4 If i > 0 Then i = 4 - i End If Dim str As String = nonce.PadRight(nonce.Length + i, "="c) Dim flag1 As Boolean = False Try Dim bs As Byte() = Convert.FromBase64String(str) dateTime = dateTime.Parse(New ASCIIEncoding().GetString(bs)) Catch e As FormatException Return False End Try flag1 = dateTime.Now <= dateTime Return flag1 End Function Protected Overridable Function GetPasswordAndRoles(ByVal app As HttpApplication, _
ByVal username As String, _
ByRef password As String, ByRef roles As String()) As Boolean password = Nothing Dim sqlConnection As sqlConnection =
New
sqlConnection(ConfigurationSettings.AppSettings("dbConn").ToString()) sqlConnection.Open() Dim sqlCommand As sqlCommand = New sqlCommand() sqlCommand.Connection = sqlConnection sqlCommand.CommandType = CommandType.StoredProcedure sqlCommand.CommandText = "AuthenticateUser" sqlCommand.Parameters.Add(New SqlParameter("@username", username)) Dim sqlDataReader As sqlDataReader = _
sqlCommand.ExecuteReader(CommandBehavior.CloseConnection) sqlDataReader.Read() password = sqlDataReader.GetString(0) Dim str2 As String = sqlDataReader.GetString(1) sqlDataReader.Close() Dim flag1 As Boolean = False roles = str2.Split(New Char() {"|"c}) If CInt(roles.Length) > 0 Then flag1 = True Else flag1 = False End If Return flag1
End Function End Class End Namespace

Not how on the OnEndRequest method, the Module makes sure to send the WWW-Authenticate header, which should bring the browser into action:

Upon being properly authenticated, we store the roles (permissions) for the newly authenticated user in the Identity Instance. Since the browser has now cached and will automatically resend its valid credentials with each request, even if we click on a hyperlink to another page, we are still authenticated, and we can capture the permissions for a user with simple, built-in classes and methods:

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  Response.Write("Howdy " & HttpContext.Current.User.Identity.Name)
  Response.Write(" Admin: " & HttpContext.Current.User.IsInRole("Administrator").ToString())
End Sub

Supporting Programmatic Authentication with Digest:

Additionally, let's talk about how to authentical with Digest programmatically: We'll create a new page, sampleCredentials.aspx, whose sole purpose is to use the WebRequest class to make a GET request to a page that is protected by our Digest authentication scheme.

First, we need to allow this page to run (assuming that for testing purposes, it is located in the same web application as the rest of the Digest Authentication pages). We do this by empolying the location path elements in our web.config, like so:

</system.web>
<location path="SampleCredentials.aspx">
<system.web><authorization>
<allow users="?" />
</authorization>
</system.web>
</location>

Note that I've placed this nodelist just below the regular closing </system.web> tag. What this says is that for the specific page only, allow anonymous access (don't challenge with Digest). This is to allow the SampleCredentials.aspx page to do the folowing:

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim query As String = "http://localhost/digestauthtestsvc/default.aspx"
Dim request As WebRequest = HttpWebRequest.Create(query)
Dim cc As CredentialCache = New CredentialCache()
request.Credentials = New NetworkCredential("test2", "test2")
Dim response As HttpWebResponse = request.GetResponse()
Dim r As StreamReader = New StreamReader(response.GetResponseStream(), Encoding.ASCII)
Page.Response.Write(r.ReadToEnd())
Response.Close()
r.Close()
End Sub

As can be seen above, it is trivial to programmatically add credentials (including for Digest Authentication) to a WebRequest instance. Indeed, the above will in fact return the Default.aspx page to the browser, indicating that it has been successfully authenticated as "test2". If we then click on the "Page 2" hperlink in the default.aspx page that is presented, we get a new login Interface dialog. Why? Simple - the digest authentication that allowed our browser to receive the contents of default.aspx (a Digest - protected page) was performed solely by the code in the WebRequest instance. The browser itself was never presented with a Digest logon challenge in this case; consequently, when it arrives on a new page that requires Digest authentication, it is appropriately required to authenticate, having never before been given this opportunity.

Should you decide to download and and try either of the below linked solultions, please bear in mind that certain preparations need to be made:

1) With the local instance of SQL Server running (and we must assume you still have the stock Northwind database), Open Query Analyzer and load the NorthwindMods.sql script. This will add the columns username varchar(15), password varchar(15) and roles varchar(250) to the Northwind Employees table. It will also create the AuthenticateUser Stored procedure, and finally will populate the Employees table with two records having user / passwords of test /test and test2/test2 respectively. Also, make sure that your Sql Server connection information squares with the dbConn AppSettings element in the web.config provided.

2) Finally, In IIS, you must make sure the virtual directory is an Application, and go into the Directory Security tab and ensure that ONLY - I repeat, ONLY Anonymous acess is on. Everything else, BASIC, Digest, etc. must be OFF. Enjoy!
The downloads below are in VS.NET 2002 format to help everybody who hasn't quite made it to 2003 yet.

3) NOTE: I'm still seeing some glitches with this on Windows Server 2003 and I haven't yet figured it out. I suspect it is related to the way IIS 6.0 comes so totally "Locked down" out of the box. However, I have gotten it to work fine using the Network Service account as the default Application Pool Identity. It should work fine on Windows 2000 and Windows XP also.

Download Digest Authentication C# Solution Download Digest Authentication VB.NET Solution

 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.