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 virtual bool GetUserByName (HttpApplication app, string username,
58 out string password, out string[] roles)
60 password = String.Empty;
61 roles = new string[0];
63 string userFileName = app.Request.MapPath (ConfigurationSettings.AppSettings ["Digest.Users"]);
64 if (userFileName == null || !File.Exists (userFileName))
67 XmlDocument userDoc = new XmlDocument ();
68 userDoc.Load (userFileName);
70 string xPath = String.Format ("/users/user[@name='{0}']", username);
71 XmlNode user = userDoc.SelectSingleNode (xPath);
76 password = user.Attributes ["password"].Value;
78 XmlNodeList roleNodes = user.SelectNodes ("role");
79 roles = new string [roleNodes.Count];
81 foreach (XmlNode xn in roleNodes)
82 roles [i++] = xn.Attributes ["name"].Value;
87 protected override bool AcceptCredentials (HttpApplication app, string authentication)
90 ListDictionary reqInfo = new ListDictionary ();
92 string[] elems = authentication.Split( new char[] {','});
93 foreach (string elem in elems) {
95 string[] parts = elem.Split (new char[] {'='}, 2);
96 string key = parts [0].Trim (new char[] {' ','\"'});
97 string val = parts [1].Trim (new char[] {' ','\"'});
98 reqInfo.Add (key,val);
101 string username = (string) reqInfo ["username"];
105 if (!GetUserByName (app, username, out password, out roles))
108 string realm = ConfigurationSettings.AppSettings ["Digest.Realm"];
110 // calculate the Digest hashes
112 // A1 = unq(username-value) ":" unq(realm-value) ":" passwd
113 string A1 = String.Format ("{0}:{1}:{2}", username, realm, password);
116 string HA1 = GetMD5HashBinHex (A1);
118 // A2 = Method ":" digest-uri-value
119 string A2 = String.Format ("{0}:{1}", app.Request.HttpMethod, (string)reqInfo["uri"]);
122 string HA2 = GetMD5HashBinHex(A2);
124 // KD(secret, data) = H(concat(secret, ":", data))
126 // request-digest = <"> < KD ( H(A1), unq(nonce-value)
128 // ":" unq(cnonce-value)
129 // ":" unq(qop-value)
132 // if qop is missing,
133 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
135 string unhashedDigest;
136 if (reqInfo["qop"] != null) {
137 unhashedDigest = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
139 (string)reqInfo["nonce"],
140 (string)reqInfo["nc"],
141 (string)reqInfo["cnonce"],
142 (string)reqInfo["qop"],
146 unhashedDigest = String.Format("{0}:{1}:{2}",
148 (string)reqInfo["nonce"],
152 string hashedDigest = GetMD5HashBinHex (unhashedDigest);
154 bool isNonceStale = !IsValidNonce((string)reqInfo["nonce"]);
155 app.Context.Items["staleNonce"] = isNonceStale;
157 bool result = (((string)reqInfo["response"] == hashedDigest) && (!isNonceStale));
159 IIdentity id = new GenericIdentity (username, AuthenticationMethod);
160 app.Context.User = new GenericPrincipal (id, roles);
165 #region Event Handlers
167 public override void OnEndRequest(object source, EventArgs eventArgs)
169 // We add the WWW-Authenticate header here, so if an authorization
170 // fails elsewhere than in this module, we can still request authentication
173 HttpApplication app = (HttpApplication) source;
174 if (app.Response.StatusCode != 401 || !AuthenticationRequired)
177 string realm = ConfigurationSettings.AppSettings ["Digest.Realm"];
178 string nonce = GetCurrentNonce ();
179 bool isNonceStale = false;
180 object staleObj = app.Context.Items ["staleNonce"];
181 if (staleObj != null)
182 isNonceStale = (bool)staleObj;
184 StringBuilder challenge = new StringBuilder ("Digest realm=\"");
185 challenge.Append(realm);
186 challenge.Append("\"");
187 challenge.Append(", nonce=\"");
188 challenge.Append(nonce);
189 challenge.Append("\"");
190 challenge.Append(", opaque=\"0000000000000000\"");
191 challenge.Append(", stale=");
192 challenge.Append(isNonceStale ? "true" : "false");
193 challenge.Append(", algorithm=MD5");
194 challenge.Append(", qop=\"auth\"");
196 app.Response.AppendHeader("WWW-Authenticate", challenge.ToString());
197 app.Response.StatusCode = 401;
202 private string GetMD5HashBinHex (string toBeHashed)
204 MD5 hash = MD5.Create ();
205 byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
207 StringBuilder sb = new StringBuilder ();
208 foreach (byte b in result)
209 sb.Append (b.ToString ("x2"));
210 return sb.ToString ();
213 protected virtual string GetCurrentNonce ()
215 DateTime nonceTime = DateTime.Now.AddSeconds (nonceLifetime);
216 byte[] expireBytes = Encoding.ASCII.GetBytes (nonceTime.ToString ("G"));
217 string nonce = Convert.ToBase64String (expireBytes);
218 // nonce can't end in '=', so trim them from the end
219 nonce = nonce.TrimEnd (trim);