2 // System.Net.DigestClient.cs
5 // Greg Reinacker (gregr@rassoc.com)
6 // Sebastien Pouliot (spouliot@motus.com)
7 // Gonzalo Paniagua Javier (gonzalo@ximian.com
9 // Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
10 // Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
11 // (c) 2003 Novell, Inc. (http://www.novell.com)
13 // Original (server-side) source code available at
14 // http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
18 using System.Collections;
19 using System.Collections.Specialized;
22 using System.Security.Cryptography;
28 // This works with apache mod_digest
33 // See RFC 2617 for details.
37 class DigestHeaderParser
42 static string [] keywords = { "realm", "opaque", "nonce", "algorithm", "qop" };
43 static char [] endSeparator = new char[] { '"', ',' };
44 string [] values = new string [keywords.Length];
46 public DigestHeaderParser (string header)
48 this.header = header.Trim ();
52 get { return values [0]; }
55 public string Opaque {
56 get { return values [1]; }
60 get { return values [2]; }
63 public string Algorithm {
64 get { return values [3]; }
68 get { return values [4]; }
73 if (!header.ToLower ().StartsWith ("digest "))
77 length = this.header.Length;
78 while (pos < length) {
80 if (!GetKeywordAndValue (out key, out value))
84 if (pos < length && header [pos] == ',')
87 int idx = Array.IndexOf (keywords, (key));
91 if (values [idx] != null)
97 if (Realm == null || Nonce == null)
103 void SkipWhitespace ()
106 while (pos < length && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
112 void SkipNonWhitespace ()
115 while (pos < length && c != ' ' && c != '\t' && c != '\r' && c != '\n') {
125 while (pos < length && header [pos] != '=') {
129 string key = header.Substring (begin, pos - begin).Trim ().ToLower ();
133 bool GetKeywordAndValue (out string key, out string value)
142 if (pos + 1 >= length || header [pos++] != '=')
146 // note: Apache doesn't use " in all case (like algorithm)
147 if (pos + 1 >= length)
149 if (header [pos] == '"')
153 pos = header.IndexOf ('"', pos);
157 value = header.Substring (beginQ, pos - beginQ);
165 static RandomNumberGenerator rng;
167 static DigestSession ()
169 rng = RandomNumberGenerator.Create ();
173 private HashAlgorithm hash;
174 private DigestHeaderParser parser;
175 private string _cnonce;
177 public DigestSession ()
182 public string Algorithm {
183 get { return parser.Algorithm; }
186 public string Realm {
187 get { return parser.Realm; }
190 public string Nonce {
191 get { return parser.Nonce; }
194 public string Opaque {
195 get { return parser.Opaque; }
199 get { return parser.QOP; }
202 public string CNonce {
204 if (_cnonce == null) {
205 // 15 is a multiple of 3 which is better for base64 because it
206 // wont end with '=' and risk messing up the server parsing
207 byte[] bincnonce = new byte [15];
208 rng.GetBytes (bincnonce);
209 _cnonce = Convert.ToBase64String (bincnonce);
210 Array.Clear (bincnonce, 0, bincnonce.Length);
216 public bool Parse (string challenge)
218 parser = new DigestHeaderParser (challenge);
219 if (!parser.Parse ()) {
220 Console.WriteLine ("Parser");
224 // build the hash object (only MD5 is defined in RFC2617)
225 if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
226 hash = HashAlgorithm.Create ("MD5");
231 private string HashToHexString (string toBeHashed)
237 byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
239 StringBuilder sb = new StringBuilder ();
240 foreach (byte b in result)
241 sb.Append (b.ToString ("x2"));
242 return sb.ToString ();
245 private string HA1 (string username, string password)
247 string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
248 if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
249 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
250 return HashToHexString (ha1);
253 private string HA2 (HttpWebRequest webRequest)
255 string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
256 if (QOP == "auth-int") {
258 // ha2 += String.Format (":{0}", hentity);
260 return HashToHexString (ha2);
263 private string Response (string username, string password, HttpWebRequest webRequest)
265 string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
267 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
268 response += HA2 (webRequest);
269 return HashToHexString (response);
272 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials)
275 throw new InvalidOperationException ();
277 HttpWebRequest request = webRequest as HttpWebRequest;
281 NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
282 string userName = cred.UserName;
283 if (userName == null || userName == "")
286 string password = cred.Password;
288 StringBuilder auth = new StringBuilder ();
289 auth.AppendFormat ("Digest username=\"{0}\", ", userName);
290 auth.AppendFormat ("realm=\"{0}\", ", Realm);
291 auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
292 auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
294 if (QOP != null) // quality of protection (server decision)
295 auth.AppendFormat ("qop=\"{0}\", ", QOP);
297 if (Algorithm != null) // hash algorithm (only MD5 in RFC2617)
298 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
301 // _nc MUST NOT change from here...
302 // number of request using this nonce
304 auth.AppendFormat ("nc={0:X8}, ", _nc);
307 // until here, now _nc can change
310 if (QOP != null) // opaque value from the client
311 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
313 if (Opaque != null) // exact same opaque value as received from server
314 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
316 auth.AppendFormat ("response=\"{0}\"", Response (userName, password, request));
317 return new Authorization (auth.ToString ());
321 class DigestClient : IAuthenticationModule
324 static Hashtable cache; // cache entries by nonce
326 static DigestClient ()
328 cache = Hashtable.Synchronized (new Hashtable ());
331 public DigestClient () {}
333 // IAuthenticationModule
335 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials)
337 if (credentials == null || challenge == null)
340 string header = challenge.Trim ();
341 if (header.ToLower ().IndexOf ("digest") == -1)
344 HttpWebRequest request = webRequest as HttpWebRequest;
348 DigestSession ds = (DigestSession) cache [request.Address];
349 bool addDS = (ds == null);
351 ds = new DigestSession ();
353 if (!ds.Parse (challenge))
357 cache.Add (request.Address, ds);
359 return ds.Authenticate (webRequest, credentials);
362 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials)
364 HttpWebRequest request = webRequest as HttpWebRequest;
368 // check cache for URI
369 DigestSession ds = (DigestSession) cache [request.Address];
373 return ds.Authenticate (webRequest, credentials);
376 public string AuthenticationType {
377 get { return "Digest"; }
380 public bool CanPreAuthenticate {