Anatoly Lubarsky Logo
programming, design, integration, games, music

Asp.Net: Authentication based on HTTP Digest protocol. Module Enhancement.

Author: Anatoly Lubarsky
Published: 22/12/2002
on aspnetmania.com


In the article "Authentication based on HTTP Basic protocol. HttpModule." I described the Basic authentication algorithm. With its help Basic Authentication role-based system was built without special confuguring of IIS server and using database to store users' credentials.


Authentication based Basic has one major downside - username and password are passed as clear text across the network, encoding base64 is not counted as secure system.


Digest Authentication


In this article, I will describe the Digest Authentication algorithm that remedies some problems, that Basic Authentication has. For example, this scheme does not pass password as clear text across the network. The official scheme name is - "Digest Access Authentication".


Let's enhance our mechanism from the previous article.


Digest has the following advantages:


  1. passwords are not passed as clear text across the network
  2. the ability to provide defense from replay attacks (monitoring http nc value)
  3. the ability to provide defense (monitoring nonce)
    • in the certain length of time
    • against a certain client
    • against a certain request

One site can use several authentication systems simultaneously (i.e. Basic and Digest).


It's time to describe the Digest Authentication algorithm:


Step #1. The first request from the User Agent to the Http Server - Http header Authorization is empty - it means that the server has to return a header, requiring a user to authenticate. For example:


Step #2. Server response:


        HTTP/1.1 401 Unauthorized
        WWW-Authenticate: Digest
                 realm="testrealm@host.com",
                 qop="auth",
                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                 opaque="5ccc069c403ebaf9f0171e9517f40e41",
                 algorithm=MD5,
                 stale=false

Let's investigate WWW-Authenticate Http response header (as you can see, it is more complicated, than the one, used in Basic mechanism):


realm A string to be displayed to users to inform them which username and password to use. For example "registered_users@gotham.news.com".
nonce A server-specified data string which should be uniquely generated each time a 401 response is made. Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed. A nonce might, for example, be constructed as the base 64 encoding of time-stamp H(time-stamp ":" ETag ":" private-key)
opaque A string of data, specified by the server, which should be returned by the client unchanged in the Authorization header of subsequent requests with URIs in the same protection space.
stale true/false
A flag, indicating that if true - the request was correct, username-password also correct, nonce was incorrect; if false or any other value or if not exists - the username-password are incorrect
algorithm optional, MD5 = default
qop If present, it is a quoted string of one or more tokens indicating the "quality of protection" values supported by the server. The value "auth" indicates authentication; the value "auth-int" indicates authentication with integrity protection.


Step #3. The user gets a modal window, inviting him or her to input the username and the password (pay attention that the window differs from the one used in the Basic mechanism). After that Authentication request is performed by the user:


        GET ... ... HTTP/1.1
        Authorization: Digest
                 username="Mufasa",
                 realm="testrealm@host.com",
                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                 uri="/dir/index.html",
                 qop=auth,
                 nc=00000001,
                 cnonce="0a4f113b",
                 response="6629fae49393a05397450978507c4ef1",
                 opaque="5ccc069c403ebaf9f0171e9517f40e41"

Now let's investigate Authorization Http request header:


username a user name
realm see WWW-Authenticate
qop see WWW-Authenticate (need to be equal to one of the list, specified in qop of WWW-Authenticate)
algorithm see WWW-Authenticate (need to be the same)
opaque see WWW-Authenticate (need to be the same)
uri a string, indicating request (for example a page)
response 32-character string - the password is checked with the help of this particular string.
nonce
nc nonce counter - specifies the number of times this specific nonce has been used.
cnonce A unique string, sent to the server by the browser.


Step #4. The sequence of operations, to maintain user's request


  1. Check if Authorization request header exists
  2. Check if it is of Digest type
  3. Cut a word "Digest"
  4. Take the username (pay attention - there is no password in the header - at least we cannot see it)
  5. Check the user against the database, if exists - get his password from the database
  6. Check the request, using role based authentication against pages, as we did using Basic authentication mechanism.

If something is wrong - go to Step #6. If everything is ok - move forward (we still have a lot of work to do :))

Step #5. Check the password

  1. Create A1 string like
    A1 = unq(username) ":" unq(realm) ":" passwd
  2. Hash A1
    HA1 = MD5(A1)
  3. Create A2 string like
    A2 = Http Method ":" digest-uri
  4. Hash A2
    HA2 = MD5(A2)
  5. Create GENRESPONSE string like
    GENRESPONSE = HA1 ":" nonce ":" nc ":" cnonce ":" qop ":" HA2
  6. Hash GENRESPONSE
    HGENRESPONSE = MD5(GENRESPONSE)

if HGENRESPONSE is equal to the response of the Authorization request header and the nonce is correct - everything is ok, if not - go to step 6


Step #6. Set the server code of state to 401, show a modal window as in Step #2, in other words - build the Digest WWW-Authenticate response header.


HTTP Module AuthDigest


Ok, being armed with the theoretical background, listed above, let's write our own HttpModule, performing Digest Authentication.


   // inherits from HttpModule
   public class AuthDigest : IHttpModule
   {
      public AuthDigest()  { }
      public void Dispose()  { }


      public void Init(HttpApplication application)
      {
         application.AuthenticateRequest += 
            new EventHandler(this.OnAuthenticateRequest);
         application.EndRequest += 
            new EventHandler(this.OnEndRequest);
      }

      // As usual, we use 2 methods:
      // OnAuthenticateRequest
      // OnEndRequest

      /*
      #####################################################
      # 
      #   OnAuthenticateRequest 
      #
      # 
      #####################################################
      */
      public void OnAuthenticateRequest(object source, 
         EventArgs eventArgs)
      {
         HttpApplication app = (HttpApplication) source;

         // get Authorization header; check if not empty
         string authorization = app.Request.Headers["Authorization"];
         if ((authorization == null) || (authorization.Length == 0))
         {
            AccessDenied(app);
            return;
         }


         // is it digest scheme ?
         authorization = authorization.Trim();
         if (authorization.IndexOf("Digest", 0) != 0)
         {
            AccessDenied(app);
            return;
         }

         // get Header parts
         // write them to the ListDictionary object
         ListDictionary dictAuthHeaderContents = 
            new ListDictionary();
         dictAuthHeaderContents = getHeaderParts(authorization);


         // check the user against the Database (by roles)
         // if everything is ok - get the password
         // this approach is the main difference 
         // from the Basic scheme
         // get user groups - 
         // necessary for the GenericPrincipal instance
         string username = 
            (string)dictAuthHeaderContents["username"];
         string password = "";
         string[] groups;
         if (!AuthenticateAgentDigest(app, 
            username, out password, out groups))
         {
            AccessDenied(app);
            return;
         }


         // see Step #5 of the Digest algorithm
         // check against Digest Scheme
         string realm = 
            ConfigurationSettings.AppSettings
            ["HTTPDigest.Components.AuthDigest_Realm"];


         // a)
         // A1 = unq(username-value) ":" unq(realm-value) ":" passwd
         string A1 = String.Format("{0}:{1}:{2}",
            (string)dictAuthHeaderContents["username"],
            realm,
            password);

         // b)
         // HA1 = MD5(A1)
         string HA1 = CvtHex(A1);

         // c)
         // A2 = HTTP Method ":" digest-uri-value
         string A2 = String.Format("{0}:{1}",
            app.Request.HttpMethod,
            (string)dictAuthHeaderContents["uri"]);

         // d)
         // HA2 = MD5(A2)
         string HA2 = CvtHex(A2);

         // e)
         // GENRESPONSE = 
         // HA1 ":" nonce ":" nc ":" cnonce ":" qop ":" HA2
         string GENRESPONSE;
         if (dictAuthHeaderContents["qop"] != null)
         {
            GENRESPONSE = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
               HA1,
               (string)dictAuthHeaderContents["nonce"],
               (string)dictAuthHeaderContents["nc"],
               (string)dictAuthHeaderContents["cnonce"],
               (string)dictAuthHeaderContents["qop"],
               HA2);
         }
         else
         {
            GENRESPONSE = String.Format("{0}:{1}:{2}",
               HA1,
               (string)dictAuthHeaderContents["nonce"],
               HA2);
         }

         string HGENRESPONSE = CvtHex(GENRESPONSE);


         // Check the nonce
         bool isNonceStale = 
            !IsValidNonce((string)dictAuthHeaderContents["nonce"]);
         app.Context.Items["staleNonce"] = isNonceStale;


         // Check HGENRESPONSE 
         // against the response of the Authorization header
         // Check the nonce
         // if everything is ok - 
         // create GenericPrincipal instance, which contains
         // users groups
         string TestResponse = 
            dictAuthHeaderContents["response"].ToString();
         if ((TestResponse == HGENRESPONSE) && 
            (!isNonceStale))
         {
            app.Context.User = 
               new GenericPrincipal(
               new GenericIdentity(username,
                  "HTTPDigest.Components.AuthDigest"), 
               groups);
         }
         else
         {
            AccessDenied(app);
            return;
         }
      }


      /*
      ######################################################
      #
      #   OnEndRequest
      # 
      #   set server response 
      #   WWW-Authenticate header (digest scheme)
      #   build header string according to scheme
      #   lift up modal window
      # 
      ######################################################
      */
      public void OnEndRequest(object source, 
         EventArgs eventArgs)
      {
         HttpApplication app = 
            (HttpApplication) source;
         if (app.Response.StatusCode == 401)
         {
            // from config.
            string lRealm = ConfigurationSettings.AppSettings
               ["HTTPDigest.Components.AuthDigest_Realm"];
            string lOpaque = ConfigurationSettings.AppSettings
               ["HTTPDigest.Components.AuthDigest_Opaque"];
            string lAlgorithm = ConfigurationSettings.AppSettings
               ["HTTPDigest.Components.AuthDigest_Algorithm"];
            string lQop = ConfigurationSettings.AppSettings
               ["HTTPDigest.Components.AuthDigest_Qop"];

            // generate
            string lNonce = GenerateNonce();

            bool isNonceStale = false;
            object staleObj = app.Context.Items["staleNonce"];
            if (staleObj != null)
               isNonceStale = (bool)staleObj;


            // Show Digest modal window
            // build WWW-Authenticate server response header
            StringBuilder authHeader = new StringBuilder("Digest");
            authHeader.Append(" realm=\"");
            authHeader.Append(lRealm);
            authHeader.Append("\"");
            authHeader.Append(", nonce=\"");
            authHeader.Append(lNonce);
            authHeader.Append("\"");
            authHeader.Append(", opaque=\"");
            authHeader.Append(lOpaque);
            authHeader.Append("\"");
            authHeader.Append(", stale=");
            authHeader.Append(isNonceStale ? "true" : "false");
            authHeader.Append(", algorithm=\"");
            authHeader.Append(lAlgorithm);
            authHeader.Append("\"");
            authHeader.Append(", qop=\"");
            authHeader.Append(lQop);
            authHeader.Append("\"");

            app.Response.AppendHeader("WWW-Authenticate",
               authHeader.ToString());

            // Set response state to 401
            app.Response.StatusCode = 401;
         }
      }


      /*
      ########################################################
      #
      #   GenerateNonce
      # 
      #   generate unique server nonce
      # 
      ########################################################
      */
      protected virtual string GenerateNonce()
      {
         // Create a unique nonce - 
         // the simpliest version
         // Now + 3 minutes, encoded base64
         // The nonce validity check 
         // will be performed also against the time
         // More strong example of nonce - 
         // use additionally ETag and unique key, which is
         // known by the server
         DateTime nonceTime = 
            DateTime.Now + TimeSpan.FromMinutes(3);
         string expireStr = nonceTime.ToString("G");

         Encoding enc = new ASCIIEncoding();
         byte[] expireBytes = enc.GetBytes(expireStr);
         string nonce = Convert.ToBase64String(expireBytes);


         // base64 adds "=" 
         // sign, which is forbidden by the server
         // cut it
         nonce = nonce.TrimEnd(new Char[] {'='});
         return nonce;
      }


      /*
      ########################################################
      #
      #   IsValidNonce
      # 
      #   string nonce : in
      # 
      ########################################################
      */
      protected virtual bool IsValidNonce(string nonce)
      {
         // Check nonce validity
         // decode from base64 and check
         // This implementation uses a simple version - 
         // thats why the check is simple also -
         // check against the time

         DateTime expireTime;
         int numPadChars = nonce.Length % 4;
         if (numPadChars > 0)
            numPadChars = 4 - numPadChars;
         string newNonce = 
            nonce.PadRight(nonce.Length + numPadChars, '=');

         try
         {
            byte[] decodedBytes = 
               Convert.FromBase64String(newNonce);
            string preNonce = 
               new ASCIIEncoding().GetString(decodedBytes);
            expireTime = DateTime.Parse(preNonce);
         }
         catch (FormatException)
         {
            return false;
         }
         return (expireTime >= DateTime.Now);
      }


      /*
      ########################################################
      #
      #   CvtHex - hashes strings
      # 
      #   string sToConvert : in
      #   string SConverted : out
      # 
      ########################################################
      */
      private string CvtHex(string sToConvert)
      {
         // Hashing
         Encoding enc = new ASCIIEncoding();
         MD5 md5 = new MD5CryptoServiceProvider();
         byte[] bToConvert = 
            md5.ComputeHash(enc.GetBytes(sToConvert));
         string sConverted = "";
         for (int i = 0 ; i < 16 ; i++)
            sConverted += 
               String.Format("{0:x02}", bToConvert[i]);
         return sConverted;
      }


      /*
      ########################################################
      #
      #   getHeaderParts(string authorization)
      # 
      #   convert Authorization header
      #   from string to Dictionary
      #   string authorization : in
      #   ListDictionary : out
      # 
      ########################################################
      */
      private ListDictionary getHeaderParts(string authorization)
      {
         // A method, that converts 
         // HTTP header string with all its contents
         // to the ListDictionary object
         ListDictionary dict = new ListDictionary();
         string[] parts = 
            authorization.Substring(7).Split(new char[] {','});
         foreach (string part in parts)
         {
            string[] subParts = part.Split(new char[] {'='}, 2);
            string key = subParts[0].Trim(new char[] {' ', '\"'});
            string val = subParts[1].Trim(new char[] {' ', '\"'});
            dict.Add(key, val);
         }
         return dict;
      }


      /*
      ########################################################
      #
      #   AccessDenied
      #   401 - Access Denied
      # 
      #   app in;  
      #   HttpApplication
      # 
      ########################################################
      */
      private void AccessDenied(HttpApplication app)
      {
         // Access denied
         // Write to the browser
         app.Response.StatusCode = 401;
         app.Response.StatusDescription = "Access Denied";
         app.Response.Write("401 Access Denied");
         app.CompleteRequest();
      }


      // The next method checks the credentials 
      // against the database
      // based on roles
      // if everything is ok, 
      // returns true and the users list of groups
      // that is necessary to create the instance of 
      // GenericPrincipal
      // + also returns his password - 
      // this is the difference from the Basic scheme


      /*
      ########################################################
      #
      #   AuthenticateAgentDigest
      #
      #   Authenticates Agent, returns true/false
      #   app in; HttpApplication
      #   User in; username
      #   Password out; password to hash then and check
      #   groups out; agent groups to create GenericPrincipal
      # 
      ########################################################
      */
      protected virtual bool 
         AuthenticateAgentDigest(HttpApplication app, 
            string username, 
            out string password, 
            out string[] groups)
      {
         password = "";
         groups = null;
         int lagentID = 0;
         string lpageURL = "";


         // instance of a class, that works with databse
         // source code attached
         SqlDataProvider dataProvider = new SqlDataProvider();

         // check if the specified user exists
         // if yes - get his password
         // get agent if exists
         lagentID = 
            dataProvider.getAgentByUsername(username, out password);
         if (lagentID == 0)
            return false;


         // check if the specified user has groups
         // get agent groups
         ArrayList arrAgentsGroups = new ArrayList();
         arrAgentsGroups = 
            dataProvider.getGroupsByAgentID(lagentID);
         if (arrAgentsGroups.Count == 0)
            return false;


         // check if requested page has groups
         // get pages groups
         lpageURL = app.Request.Path;
         ArrayList arrPagesGroups = new ArrayList();
         arrPagesGroups = 
            dataProvider.getGroupsByPageURL(lpageURL);
         if (arrPagesGroups.Count == 0)
            return false;


         // check if at least one user group 
         // is in Page Groups List
         // if yes, return true
         // check if at least one agent group 
         // is in Page Groups List
         string[] pagegroups = 
            (string[])arrPagesGroups.ToArray(typeof(string));
         groups = 
            (string[]) arrAgentsGroups.ToArray(typeof(string));

         foreach (string groupagentID in groups)
         {
            foreach (string grouppageID in pagegroups)
            {
               if (groupagentID == grouppageID)
                  return true;
            }
         }
         return false;
      }
   }

That's it. HttpModule is ready. Let's create a configuration repository of it. In web.config under system.web let's write:


   < HttpModules >
      < add type="HttpDigest.Components.AuthDigest, HttpDigest"
         name="DigestAuthenticationModule" / >

Cancelling built-in authentication


   < Authentication mode="None" / >
      < Authorization >
         < deny users="?" / >

And save the essential data used by the Digest Authentication scheme (realm string, algorithm, qop string, server opaque)


   < AppSettings >
      < add value="testrealm@host.com"
         key="HttpDigest.Components.AuthDigest_Realm" / >
      < add value="5ccc069c403ebaf9f0171e9517f40e41"
         key="HttpDigest.Components.AuthDigest_Opaque" / >
      < add value="MD5"
         key="HttpDigest.Components.AuthDigest_Algorithm" / >
      < add value="auth"
         key="HttpDigest.Components.AuthDigest_Qop" / >

Thats all.


The Digest Access Authentication scheme is not intended to be a complete answer to the need for security on the World Wide Web. Digest Authentication offers no confidentiality protection beyond protecting the actual password. However, it is significantly stronger than (e.g.) CRAM-MD5, which has been proposed for use with LDAP, POP and IMAP. It is intended to provide an authentication system, simple and flexible as Basic, and at the same time to remedy the weaknesses of the Basic authentication mechanism.


Class, that works with the database and database scripts are inside the source code, is attached.


Materials and credits: rfc2617, Marty Hall, Greg Reinacker, aspnetmania.com


Related Posts:

Saturday, November 29, 2003 5:49 AM

Comments

# re: Authentication System Based on HTTP - Digest protocol. Module Enhancement.
Can we get C code for this...

6/7/2004 6:02 PM by AdiNarayana K

# re: Asp.Net: Authentication based on HTT
As far as I can see this code requires that passwords are stored in clear text.
Is it possible to change the code to support passwords that are stored MD5 hased?

1/10/2006 10:32 AM by Casper Skovgaard

# re: Asp.Net: Authentication based on HTTP Digest protocol. Module Enhancement.
Casper: nope... Digest authentication does not send clear text passwords, they are MD5 hashed.

1/11/2006 1:59 AM by Anatoly Lubarsky

# re: Asp.Net: Authentication based on HTT
Thanks Anatoly,

I'll use the Basic authentication guide instead.

1/11/2006 1:56 PM by Casper Skovgaard

# re: Asp.Net: Authentication based on HTTP Digest protocol. Module Enhancement.
This doesn't work consistently on IIS 6, anyone else seen this?

11/20/2008 12:49 AM by JMan

Login

Subscribe via RSS

Article Categories

.Net Framework
ASP.NET Tips
C# Win32 API
HTML, CSS, Web
Javascript Tips
MSSQL Tips
System
System.Net
WebServices

Archives

(02) January 2018
(01) June 2013
(03) March 2013
(02) February 2013
(01) July 2012
(01) April 2012
(01) September 2011
(01) August 2011
(03) May 2011
(01) December 2010
(01) November 2010
(01) October 2010
(01) June 2010
(01) May 2010
(02) March 2010
(01) January 2010
(02) December 2009
(03) September 2009
(03) August 2009
(09) July 2009
(04) June 2009
(03) May 2009
(02) April 2009
(03) March 2009
(02) February 2009
(02) January 2009
(04) December 2008
(04) November 2008
(05) October 2008
(04) September 2008
(05) August 2008
(04) July 2008
(05) June 2008
(07) May 2008
(04) April 2008
(03) March 2008
(02) February 2008
(03) January 2008
(03) December 2007
(05) November 2007
(04) October 2007
(05) September 2007
(12) August 2007
(11) July 2007
(14) June 2007
(13) May 2007
(13) April 2007
(10) March 2007
(11) February 2007
(14) January 2007
(14) December 2006
(12) November 2006
(08) October 2006
(09) September 2006
(06) August 2006
(08) July 2006
(10) June 2006
(09) May 2006
(22) April 2006
(25) March 2006
(12) February 2006
(14) January 2006
(19) December 2005
(17) November 2005
(16) October 2005
(16) September 2005
(12) August 2005
(14) July 2005
(09) June 2005
(12) May 2005
(12) April 2005
(20) March 2005
(11) February 2005
(12) January 2005
(18) December 2004
(13) November 2004
(12) October 2004
(14) September 2004
(09) August 2004
(23) July 2004
(19) June 2004
(29) May 2004
(19) April 2004
(16) March 2004
(09) February 2004
(06) January 2004
(02) December 2003
(01) November 2003

Post Categories

.Net and C#
Android
Antispam
App. Development
Architecture
ASP.NET
Blogging
Deprecated Projects
Facebook Platform
Fun
Google
iOS
Javascript
Misc.
MSSQL
Music
My Games
Performance
Roller
Social Networks
Tools
Visual Studio
Web 2.0
WebServices

About Me

linkedin Profile
Recs
Who am I

My Sites

Billy Beet
x2line blogs