2 // Digest Authentication implementation
5 // Greg Reinacker (gregr@rassoc.com)
6 // Sebastien Pouliot (spouliot@motus.com)
8 // Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
9 // Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
11 // Original source code available at
12 // http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
16 using System.Collections.Specialized;
17 using System.Configuration;
19 using System.Security.Cryptography;
20 using System.Security.Principal;
25 namespace Mono.Http.Modules
27 public class DigestAuthenticationModule : AuthenticationModule
29 // TODO: Digest.Nonce.Lifetime="0" Never expires
30 static int nonceLifetime = 60;
31 static char[] trim = {'='};
33 public DigestAuthenticationModule () : base ("Digest") {}
35 protected virtual bool IsValidNonce (string nonce)
39 // pad nonce on the right with '=' until length is a multiple of 4
40 int numPadChars = nonce.Length % 4;
42 numPadChars = 4 - numPadChars;
43 string newNonce = nonce.PadRight(nonce.Length + numPadChars, '=');
46 byte[] decodedBytes = Convert.FromBase64String(newNonce);
47 string expireStr = new ASCIIEncoding().GetString(decodedBytes);
48 expireTime = DateTime.Parse(expireStr);
50 catch (FormatException e) {
54 return (DateTime.Now <= expireTime);
57 protected override bool AcceptCredentials (HttpApplication app, string authentication)
60 ListDictionary reqInfo = new ListDictionary ();
62 string[] elems = authentication.Split( new char[] {','});
63 foreach (string elem in elems) {
65 string[] parts = elem.Split (new char[] {'='}, 2);
66 string key = parts [0].Trim (new char[] {' ','\"'});
67 string val = parts [1].Trim (new char[] {' ','\"'});
68 reqInfo.Add (key,val);
71 string username = (string) reqInfo ["username"];
73 string userFileName = app.Request.MapPath (ConfigurationSettings.AppSettings ["Digest.Users"]);
74 if (userFileName == null || !File.Exists (userFileName))
77 XmlDocument userDoc = new XmlDocument ();
78 userDoc.Load (userFileName);
80 string xPath = String.Format ("/users/user[@name='{0}']", username);
81 XmlNode user = userDoc.SelectSingleNode (xPath);
86 string password = user.Attributes ["password"].Value;
87 string realm = ConfigurationSettings.AppSettings ["Digest.Realm"];
89 // calculate the Digest hashes
91 // A1 = unq(username-value) ":" unq(realm-value) ":" passwd
92 string A1 = String.Format ("{0}:{1}:{2}", username, realm, password);
95 string HA1 = GetMD5HashBinHex (A1);
97 // A2 = Method ":" digest-uri-value
98 string A2 = String.Format ("{0}:{1}", app.Request.HttpMethod, (string)reqInfo["uri"]);
101 string HA2 = GetMD5HashBinHex(A2);
103 // KD(secret, data) = H(concat(secret, ":", data))
105 // request-digest = <"> < KD ( H(A1), unq(nonce-value)
107 // ":" unq(cnonce-value)
108 // ":" unq(qop-value)
111 // if qop is missing,
112 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
114 string unhashedDigest;
115 if (reqInfo["qop"] != null) {
116 unhashedDigest = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
118 (string)reqInfo["nonce"],
119 (string)reqInfo["nc"],
120 (string)reqInfo["cnonce"],
121 (string)reqInfo["qop"],
125 unhashedDigest = String.Format("{0}:{1}:{2}",
127 (string)reqInfo["nonce"],
131 string hashedDigest = GetMD5HashBinHex (unhashedDigest);
133 bool isNonceStale = !IsValidNonce((string)reqInfo["nonce"]);
134 app.Context.Items["staleNonce"] = isNonceStale;
136 bool result = (((string)reqInfo["response"] == hashedDigest) && (!isNonceStale));
138 XmlNodeList roleNodes = user.SelectNodes ("role");
139 string[] roles = new string [roleNodes.Count];
141 foreach (XmlNode xn in roleNodes)
142 roles [i++] = xn.Attributes ["name"].Value;
144 IIdentity id = new GenericIdentity (username, AuthenticationMethod);
145 app.Context.User = new GenericPrincipal (id, roles);
150 #region Event Handlers
152 public override void OnEndRequest(object source, EventArgs eventArgs)
154 // We add the WWW-Authenticate header here, so if an authorization
155 // fails elsewhere than in this module, we can still request authentication
158 HttpApplication app = (HttpApplication) source;
159 if (app.Response.StatusCode != 401 || !AuthenticationRequired)
162 string realm = ConfigurationSettings.AppSettings ["Digest.Realm"];
163 string nonce = GetCurrentNonce ();
164 bool isNonceStale = false;
165 object staleObj = app.Context.Items ["staleNonce"];
166 if (staleObj != null)
167 isNonceStale = (bool)staleObj;
169 StringBuilder challenge = new StringBuilder ("Digest realm=\"");
170 challenge.Append(realm);
171 challenge.Append("\"");
172 challenge.Append(", nonce=\"");
173 challenge.Append(nonce);
174 challenge.Append("\"");
175 challenge.Append(", opaque=\"0000000000000000\"");
176 challenge.Append(", stale=");
177 challenge.Append(isNonceStale ? "true" : "false");
178 challenge.Append(", algorithm=MD5");
179 challenge.Append(", qop=\"auth\"");
181 app.Response.AppendHeader("WWW-Authenticate", challenge.ToString());
182 app.Response.StatusCode = 401;
187 private string GetMD5HashBinHex (string toBeHashed)
189 MD5 hash = MD5.Create ();
190 byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
192 StringBuilder sb = new StringBuilder ();
193 foreach (byte b in result)
194 sb.Append (b.ToString ("x2"));
195 return sb.ToString ();
198 protected virtual string GetCurrentNonce ()
200 DateTime nonceTime = DateTime.Now.AddSeconds (nonceLifetime);
201 byte[] expireBytes = Encoding.ASCII.GetBytes (nonceTime.ToString ("G"));
202 string nonce = Convert.ToBase64String (expireBytes);
203 // nonce can't end in '=', so trim them from the end
204 nonce = nonce.TrimEnd (trim);