VB.NET: Single Sign-On with DIGEST, BASIC and LDAP
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version

Peter Bromberg

In my article about ASP.NET Digest authentication here, I published a piece with an HttpModule for Digest Authentication in VB.NET, which was based primarily on an original piece in C# done by Greg Reinacker on his blog page. Since then, I've received a number of thanks for wiring up the database code and providing a schema and a sample test web application. One of the most common requests I received has been to provide a BASIC Authentication module also written in VB.NET. Now, personally, I prefer to use C# for almost all of my .NET coding, but since the original article I did had a VB.NET version, I felt I had an obligation to more or less "complete the picture". And so I have. In fact, I threw in LDAP just for good measure! And to keep things neat and clean, all three of them are in the same assembly.



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>

Your <appSettings> section should look as follows:

<appSettings>
<add key="Realm" value="TestRealm" />
<add key="dbConn" value="Server=(local);DataBase=UserManager;User Id=sa;Password=;" />
<add key="useLDAPServer" value="false" />
<add key="Path" value="LDAP://CN=Peter,DC=Instance1,DC=COM" />
</appSettings>

If you set the useLDAPServer value to "true" and provide a valid path such as "LDAP://myDomainBox / " then the LDAP GetPasswordAndRoles method will kick in. It searches on the standard schema search.Filter = "(SAMAccountName=" + username + ")", but you can change this to a custom filter if you want.

Single Sign-On

Now the coolest thing about using an HttpModule for either DIGEST or BASIC Authentication against your own Data Store is that as long as the HttpModule is configured at all the destination sites / applications, and as long as each location has access to the same database information ( The "Users" table), you can authenticate once, and travel anywhere you want without having to ever see a logon dialog again! Digest, as most people are aware, is a lot more secure than BASIC because there is no cookie and the password information is never sent "in the clear". If you actually want to see everything the browser is sending and what's going on during the authentication process, all you need to do is set the Trace element in the web.config to "true"- you don't need any special programs.

Cache as Cache Can

While I was putting this all together, I reasoned, "Why couldn't we cache an authentication, and save all those trips to the database?". So I wired up the GetPasswordAndRoles method as follows:

Dim strPass As String = username & "_pass)"
If Not HttpContext.Current.Cache(username) Is Nothing Then
HttpContext.Current.Trace.Write("cache was used")
roles = HttpContext.Current.Cache(username)
password = HttpContext.Current.Cache(strPass)
Return True
End If

In other words, if this person already authenticated successfully, then instead of getting their password and roles from the database, there should be no reason why we can't keep them around in memory for say, one day. Now at the end of the method:

If CInt(roles.Length) > 0 Then
HttpContext.Current.Trace.Write("Saving Roles" & strRoles)
HttpContext.Current.Trace.Write("cache NOT used")
HttpContext.Current.Cache.Add(username, roles, Nothing, DateTime.MaxValue, TimeSpan.FromDays(1), Caching.CacheItemPriority.Normal, Nothing)
HttpContext.Current.Cache.Add(strPass, password, Nothing, DateTime.MaxValue, TimeSpan.FromDays(1), Caching.CacheItemPriority.Normal, Nothing)
Return True
Else
Return False
End If

Debugging Notes

Please note that you will not be able to run this solution in regular Debug mode, because in order for it to work, native IIS Windows Authentication must be turned off. You are using your own custom scheme, and after all, you cannot expect to have both on at the same time, or yours would get "hijacked". You can intersperse your code statements, however, with Trace.Write statements to indicate values, etc. I have Trace turned off in the web.config supplied. In order to run the project, you need to select "Start without debugging" in the IDE. Of course, many enterprising developers will note that we can still debug this anyway by using Debug -->Processes-->Attach and attaching to the ASPNET_WP.EXE process during execution. Sometimes it can be helpful to set a breakpoint early in the HTTP pipeline to allow one to do this before resuming debugging.

SETUP INSTRUCTIONS FOR THE SOLUTION:

•  Unzip the supplied zip file into the folder of your choice.
•  In IIS Manager, create a new Virtual Directory on the subfolder "DigestAuthModWeb2".
•  Make sure this Virtual Directory is marked as an IIS Application (In the Application Settings section, "Application Name" must be filled in).
•  In the Directory Security Tab, click the EDIT button, and ensure that ONLY Anonymous authentication is checked.
•  Ensure that a copy of the DigestAuthMod.dll assembly is in the /bin folder of your Web application.
•  Run the SetUpDataBase.sql script in SQL Query Analyzer to set up the UserManager database with a Users table, two sample records with username password combos of "test1 " and "test2 " respectively, and the required AuthenticateUser Stored procedure. The "test1 " user will have roles of Admnistrator, Manager, and User. The test2 user will have only a User role. In production of course, you would add your own users and roles. Roles are stored as a pipe-delimited string in the Roles column: "Administrator|Manager|User "
•  Modify your web.config as follows:
Authentication mode element should be set to "None ":
<authentication mode="None" />
You can set Authorization elements if you wish, for example:
<authorization>
<deny users="?" />
</authorization>

Just above the closing </System.Web> tag, set up your HttpModules:

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

The above will set up both Digest and Basic authentication. Both use the same database and Users Table. Should a non-Digest compliant browser request your site, DIGEST will automatically degrade gracefully to BASIC authentication for them. BASIC, DIGEST and optional LDAP Authentication all reside in the same assembly.

Just below the closing </System.Web> tag, add your <appSettings> Section as follows:
<appSettings>
<add key="Realm" value="YourDesiredRealm" />
<add key="dbConn" value="Server=(local);DataBase=UserManager;User Id=sa;Password=;" />
</appSettings>

You may need to modify the above "dbConn " entry to match your actual database setup. You can additionally control access to certain folders or files based on users or roles. Here is an example (included in the sample application) that allows only authenticated Administrators and Managers access to the pages in the "Manager " subfolder of the site:

<location path="manager">
<system.web>
<authorization>
<allow roles="Manager,Administrator" />
<deny users="*" />
</authorization>
</system.web>
</location>

8. Open Your browser and request the Http://localhost/DigestAuthModWeb2/ web site.

You should be presented with a login dialog like the following:

 

 

 

You can now login ( "User Name/Password ") as either test1 / test1 or test2 / test2 and try out the other features of the mechanism. Test1 is a member of Administrator, Manager and User, while test2 is only a member of User. Consequently, you should only be able to navigate to the Manager page if logged in as test1.

NOTE: Windows XP machines that are on a Domain do not enable turning off DIGEST Authentication through the IIS Manager. You can use the VBS script below to achieve this:
' Select the virtual directory you want to modify, you'd need to determine the
' site number if you're running more than 1 site. The "/1/ " in the RootNodePath denotes site "1 ".
' append a virtual directory name on next line to specify only a subdirectory

RootNodePath = "IIS://LocalHost/w3svc/1/Root"
Set oRootNode = GetObject(RootNodePath)
If Err <> 0 Then
Display "Couldn't find the path " & RootNodePath & "!"
WScript.Quit (1)
End If
oRootNode.AuthFlags = 1
' turn off all authentication except Anonymous
oRootNode.SetInfo


The AuthFlags argument is a bitmask containing the authentication options for the given object, where 1 = Anonymous, 2 = Basic, 4 = NTLM and 16 = Digest. So this example sets the authentication to Anonymous.

Information on Digest Authentication  

With Digest Authentication the client makes an un-authenticated request to the server, and the server responds with a 401 response indicating that it supports Digest authentication.  The server also sends a nonce , which can be thought of as an opaque token.  The client then re-requests the resource, sending up the username, and a cryptographic hash of the password combined with the nonce value.  The server then generates the hash itself, and if it matches the request's hash, the request is allowed. With this implementation, we request the password from the database based on the passed - in username, and are able to compute the hash. We also cache the password and list of roles for each authenticated user.

Here is some more detail:

The client first makes a request (following is abbreviated for clarity):

GET /FooSite/Default.aspx HTTP/1.1
Accept: */*
Host: localhost:8100
Connection: Keep-Alive

The server responds with a challenge:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="SampleSite", nonce="Ny8yLzIwMDIgMzoyNjoyNCBQTQ", opaque="0000000000000000", stale=false, algorithm=MD5, qop="auth"

Here is the WWW-Authenticate line in more detail:

•  Digest - specifies that the server supports Digest authentication

•  realm="SampleSite" - specifies the authentication realm.  This is intended to give the client an idea of which credentials are being requested.

•  nonce="Ny8yLzIwMDIgMzoyNjoyNCBQTQ" - specifies the nonce value the client must use for an authenticated request.

•  opaque="0000000000000000" - an opaque value that the server needs the client to pass back to it unchanged.  This implementation does not use this.

•  stale=false - indicates that the previous request was not denied because of a "stale" nonce.  If this parameter was true, it would mean that the request looked ok, and the credentials were correct, but the nonce was invalid.

•  algorithm=MD5 - specifies the hash algorithm to use when computing the digest.

•  qop="auth" - indicates "quality of protection".  "auth" means authentication only, and "auth-int" means authentication plus integrity protection.  This implementation sends "auth".

The client then sends an authenticated request:

GET /FooSite/Default.aspx HTTP/1.1
Accept: */*
Host: localhost:8100
Authorization: Digest username="test", realm="SampleSite", qop="auth", algorithm="MD5", uri= "/FooSite/Default.aspx", nonce="Ny8yLzIwMDIgMzoyNjoyNCBQTQ", nc=00000001, cnonce="c51b5139556f939768f770dab8e5277a", opaque="0000000000000000", response="afa30c6445a14e2817a423ca4a143792"

Here is the Authorization header in more detail:

•  Digest - indicates that this is a Digest authentication header.

•  username="test" - the user name.

•  realm="SampleSite" - the authentication realm, specified in the WWW-Authenticate challenge.

•  qop="auth" - the requested quality of protection; should match the challenge.

•  algorithm="MD5" - the hash algorithm used to calculate the digest.  Should match the challenge.

•  uri= "/FooSite/Default.aspx" - specifies the requested URI on the server.  It is repeated here to ensure interoperability through proxies.

•  nonce="Ny8yLzIwMDIgMzoyNjoyNCBQTQ" - the nonce value used for the request.

•  nc=00000001 - indicates the number of requests the client has made using this particular nonce value.  This information can be used by the server to protect against replay attacks; it is not used in this implementation.

•  cnonce="c51b5139556f939768f770dab8e5277a" - opaque value generated by the client.

•  opaque="0000000000000000" - opaque value from the challenge.

•  response="afa30c6445a14e2817a423ca4a143792" - the 32-character digest.

The digest is calculated based on the username, the password, the realm, the nonce, the nc value, the cnonce value, the qop value, and the uri value.  For more details on the actual hash calculation, refer to the Digest Authentication Specification at: http://www.ietf.org/rfc/rfc2617.txt?number=2617

The important advantages of Digest authentication are:

•  The password is never transmitted in clear-text, in contrast to Basic authentication.

•  The server can choose and restrict nonce values, This provides resistance to replay attacks.

•  No cookies are used or necessary, all credential information is transmitted by the browser in the headers.

Implementation Details

In this implementation, nonce values provide a minimum level of replay attack protection while permitting the maximum response time and throughput.  The nonce itself is derived from the base64 encoding of the text representation of the nonce expiration time, which is 1 minute after the current server time (e.g., Dim nonceTime As DateTime = DateTime . Now . AddMinutes (1) )

A nonce handed out by the server will be valid for 60 seconds from the issue time.  If a client makes a request after the 60 seconds are up, a 401 response will be sent with a stale=true property in the WWW-Authenticate header. Once again I want to express my thanks to Greg Reinacker, who published the original HttpModule code in C# that this enhanced module is based on.

Download the Source Code that accompanies this article

 

 

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.