1 // 2 // Digest Authentication implementation 3 // 4 // Authors: 5 // Greg Reinacker (gregr@rassoc.com) 6 // Sebastien Pouliot (spouliot@motus.com) 7 // 8 // Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved. 9 // Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com) 10 // 11 // Original source code available at 12 // http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html 13 // 14 15 // 16 // Permission is hereby granted, free of charge, to any person obtaining 17 // a copy of this software and associated documentation files (the 18 // "Software"), to deal in the Software without restriction, including 19 // without limitation the rights to use, copy, modify, merge, publish, 20 // distribute, sublicense, and/or sell copies of the Software, and to 21 // permit persons to whom the Software is furnished to do so, subject to 22 // the following conditions: 23 // 24 // The above copyright notice and this permission notice shall be 25 // included in all copies or substantial portions of the Software. 26 // 27 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 28 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 30 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 31 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 32 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 33 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 // 35 36 using System; 37 using System.Collections.Specialized; 38 using System.Configuration; 39 using System.IO; 40 using System.Security.Cryptography; 41 using System.Security.Principal; 42 using System.Text; 43 using System.Web; 44 using System.Xml; 45 46 namespace Mono.Http.Modules 47 { 48 public class DigestAuthenticationModule : AuthenticationModule 49 { 50 // TODO: Digest.Nonce.Lifetime="0" Never expires 51 static int nonceLifetime = 60; 52 static char[] trim = {'='}; 53 DigestAuthenticationModule()54 public DigestAuthenticationModule () : base ("Digest") {} 55 IsValidNonce(string nonce)56 protected virtual bool IsValidNonce (string nonce) 57 { 58 DateTime expireTime; 59 60 // pad nonce on the right with '=' until length is a multiple of 4 61 int numPadChars = nonce.Length % 4; 62 if (numPadChars > 0) 63 numPadChars = 4 - numPadChars; 64 string newNonce = nonce.PadRight(nonce.Length + numPadChars, '='); 65 66 try { 67 byte[] decodedBytes = Convert.FromBase64String(newNonce); 68 string expireStr = new ASCIIEncoding().GetString(decodedBytes); 69 expireTime = DateTime.Parse(expireStr); 70 } 71 catch (FormatException) { 72 return false; 73 } 74 75 return (DateTime.Now <= expireTime); 76 } 77 GetUserByName(HttpApplication app, string username, out string password, out string[] roles)78 protected virtual bool GetUserByName (HttpApplication app, string username, 79 out string password, out string[] roles) 80 { 81 password = String.Empty; 82 roles = new string[0]; 83 84 string userFileName = app.Request.MapPath (ConfigurationSettings.AppSettings ["Digest.Users"]); 85 if (userFileName == null || !File.Exists (userFileName)) 86 return false; 87 88 XmlDocument userDoc = new XmlDocument (); 89 userDoc.Load (userFileName); 90 91 string xPath = String.Format ("/users/user[@name='{0}']", username); 92 XmlNode user = userDoc.SelectSingleNode (xPath); 93 94 if (user == null) 95 return false; 96 97 password = user.Attributes ["password"].Value; 98 99 XmlNodeList roleNodes = user.SelectNodes ("role"); 100 roles = new string [roleNodes.Count]; 101 int i = 0; 102 foreach (XmlNode xn in roleNodes) 103 roles [i++] = xn.Attributes ["name"].Value; 104 105 return true; 106 } 107 AcceptCredentials(HttpApplication app, string authentication)108 protected override bool AcceptCredentials (HttpApplication app, string authentication) 109 { 110 // digest 111 ListDictionary reqInfo = new ListDictionary (); 112 113 string[] elems = authentication.Split( new char[] {','}); 114 foreach (string elem in elems) { 115 // form key="value" 116 string[] parts = elem.Split (new char[] {'='}, 2); 117 string key = parts [0].Trim (new char[] {' ','\"'}); 118 string val = parts [1].Trim (new char[] {' ','\"'}); 119 reqInfo.Add (key,val); 120 } 121 122 string username = (string) reqInfo ["username"]; 123 string password; 124 string[] roles; 125 126 if (!GetUserByName (app, username, out password, out roles)) 127 return false; 128 129 string realm = ConfigurationSettings.AppSettings ["Digest.Realm"]; 130 131 // calculate the Digest hashes 132 133 // A1 = unq(username-value) ":" unq(realm-value) ":" passwd 134 string A1 = String.Format ("{0}:{1}:{2}", username, realm, password); 135 136 // H(A1) = MD5(A1) 137 string HA1 = GetMD5HashBinHex (A1); 138 139 // A2 = Method ":" digest-uri-value 140 string A2 = String.Format ("{0}:{1}", app.Request.HttpMethod, (string)reqInfo["uri"]); 141 142 // H(A2) 143 string HA2 = GetMD5HashBinHex(A2); 144 145 // KD(secret, data) = H(concat(secret, ":", data)) 146 // if qop == auth: 147 // request-digest = <"> < KD ( H(A1), unq(nonce-value) 148 // ":" nc-value 149 // ":" unq(cnonce-value) 150 // ":" unq(qop-value) 151 // ":" H(A2) 152 // ) <"> 153 // if qop is missing, 154 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> 155 156 string unhashedDigest; 157 if (reqInfo["qop"] != null) { 158 unhashedDigest = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", 159 HA1, 160 (string)reqInfo["nonce"], 161 (string)reqInfo["nc"], 162 (string)reqInfo["cnonce"], 163 (string)reqInfo["qop"], 164 HA2); 165 } 166 else { 167 unhashedDigest = String.Format("{0}:{1}:{2}", 168 HA1, 169 (string)reqInfo["nonce"], 170 HA2); 171 } 172 173 string hashedDigest = GetMD5HashBinHex (unhashedDigest); 174 175 bool isNonceStale = !IsValidNonce((string)reqInfo["nonce"]); 176 app.Context.Items["staleNonce"] = isNonceStale; 177 178 bool result = (((string)reqInfo["response"] == hashedDigest) && (!isNonceStale)); 179 if (result) { 180 IIdentity id = new GenericIdentity (username, AuthenticationMethod); 181 app.Context.User = new GenericPrincipal (id, roles); 182 } 183 return result; 184 } 185 186 #region Event Handlers 187 OnEndRequest(object source, EventArgs eventArgs)188 public override void OnEndRequest(object source, EventArgs eventArgs) 189 { 190 // We add the WWW-Authenticate header here, so if an authorization 191 // fails elsewhere than in this module, we can still request authentication 192 // from the client. 193 194 HttpApplication app = (HttpApplication) source; 195 if (app.Response.StatusCode != 401 || !AuthenticationRequired) 196 return; 197 198 string realm = ConfigurationSettings.AppSettings ["Digest.Realm"]; 199 string nonce = GetCurrentNonce (); 200 bool isNonceStale = false; 201 object staleObj = app.Context.Items ["staleNonce"]; 202 if (staleObj != null) 203 isNonceStale = (bool)staleObj; 204 205 StringBuilder challenge = new StringBuilder ("Digest realm=\""); 206 challenge.Append(realm); 207 challenge.Append("\""); 208 challenge.Append(", nonce=\""); 209 challenge.Append(nonce); 210 challenge.Append("\""); 211 challenge.Append(", opaque=\"0000000000000000\""); 212 challenge.Append(", stale="); 213 challenge.Append(isNonceStale ? "true" : "false"); 214 challenge.Append(", algorithm=MD5"); 215 challenge.Append(", qop=\"auth\""); 216 217 app.Response.AppendHeader("WWW-Authenticate", challenge.ToString()); 218 app.Response.StatusCode = 401; 219 } 220 221 #endregion 222 GetMD5HashBinHex(string toBeHashed)223 private string GetMD5HashBinHex (string toBeHashed) 224 { 225 MD5 hash = MD5.Create (); 226 byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed)); 227 228 StringBuilder sb = new StringBuilder (); 229 foreach (byte b in result) 230 sb.Append (b.ToString ("x2")); 231 return sb.ToString (); 232 } 233 GetCurrentNonce()234 protected virtual string GetCurrentNonce () 235 { 236 DateTime nonceTime = DateTime.Now.AddSeconds (nonceLifetime); 237 byte[] expireBytes = Encoding.ASCII.GetBytes (nonceTime.ToString ("G")); 238 string nonce = Convert.ToBase64String (expireBytes); 239 // nonce can't end in '=', so trim them from the end 240 nonce = nonce.TrimEnd (trim); 241 return nonce; 242 } 243 } 244 } 245