|
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
|