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 string [] values = new string [keywords.Length];
45 public DigestHeaderParser (string header)
47 this.header = header.Trim ();
51 get { return values [0]; }
54 public string Opaque {
55 get { return values [1]; }
59 get { return values [2]; }
62 public string Algorithm {
63 get { return values [3]; }
67 get { return values [4]; }
72 if (!header.ToLower ().StartsWith ("digest "))
76 length = this.header.Length;
77 while (pos < length) {
79 if (!GetKeywordAndValue (out key, out value))
83 if (pos < length && header [pos] == ',')
86 int idx = Array.IndexOf (keywords, (key));
90 if (values [idx] != null)
96 if (Realm == null || Nonce == null)
102 void SkipWhitespace ()
105 while (pos < length && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
111 void SkipNonWhitespace ()
114 while (pos < length && c != ' ' && c != '\t' && c != '\r' && c != '\n') {
124 while (pos < length && header [pos] != '=') {
128 string key = header.Substring (begin, pos - begin).Trim ().ToLower ();
132 bool GetKeywordAndValue (out string key, out string value)
141 if (pos + 1 >= length || header [pos++] != '=')
145 if (pos + 1 >= length || header [pos++] != '"')
149 pos = header.IndexOf ('"', pos);
153 value = header.Substring (beginQ, pos - beginQ);
161 static RandomNumberGenerator rng;
163 static DigestSession ()
165 rng = RandomNumberGenerator.Create ();
169 private HashAlgorithm hash;
170 private DigestHeaderParser parser;
171 private string _cnonce;
173 public DigestSession ()
178 public string Algorithm {
179 get { return parser.Algorithm; }
182 public string Realm {
183 get { return parser.Realm; }
186 public string Nonce {
187 get { return parser.Nonce; }
190 public string Opaque {
191 get { return parser.Opaque; }
195 get { return parser.QOP; }
198 public string CNonce {
200 if (_cnonce == null) {
201 // 15 is a multiple of 3 which is better for base64 because it
202 // wont end with '=' and risk messing up the server parsing
203 byte[] bincnonce = new byte [15];
204 rng.GetBytes (bincnonce);
205 _cnonce = Convert.ToBase64String (bincnonce);
206 Array.Clear (bincnonce, 0, bincnonce.Length);
212 public bool Parse (string challenge)
214 parser = new DigestHeaderParser (challenge);
215 if (!parser.Parse ()) {
216 Console.WriteLine ("Parser");
220 // build the hash object (only MD5 is defined in RFC2617)
221 if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
222 hash = HashAlgorithm.Create ("MD5");
227 private string HashToHexString (string toBeHashed)
233 byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
235 StringBuilder sb = new StringBuilder ();
236 foreach (byte b in result)
237 sb.Append (b.ToString ("x2"));
238 return sb.ToString ();
241 private string HA1 (string username, string password)
243 string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
244 if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
245 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
246 return HashToHexString (ha1);
249 private string HA2 (HttpWebRequest webRequest)
251 string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
252 if (QOP == "auth-int") {
254 // ha2 += String.Format (":{0}", hentity);
256 return HashToHexString (ha2);
259 private string Response (string username, string password, HttpWebRequest webRequest)
261 string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
263 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
264 response += HA2 (webRequest);
265 return HashToHexString (response);
268 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials)
271 throw new InvalidOperationException ();
273 HttpWebRequest request = webRequest as HttpWebRequest;
277 NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
278 string userName = cred.UserName;
279 if (userName == null || userName == "")
282 string password = cred.Password;
284 StringBuilder auth = new StringBuilder ();
285 auth.AppendFormat ("Digest username=\"{0}\", ", userName);
286 auth.AppendFormat ("realm=\"{0}\", ", Realm);
287 auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
288 auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
290 if (QOP != null) // quality of protection (server decision)
291 auth.AppendFormat ("qop=\"{0}\", ", QOP);
293 if (Algorithm != null) // hash algorithm (only MD5 in RFC2617)
294 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
297 // _nc MUST NOT change from here...
298 // number of request using this nonce
300 auth.AppendFormat ("nc={0:X8}, ", _nc);
303 // until here, now _nc can change
306 if (QOP != null) // opaque value from the client
307 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
309 if (Opaque != null) // exact same opaque value as received from server
310 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
312 auth.AppendFormat ("response=\"{0}\"", Response (userName, password, request));
313 return new Authorization (auth.ToString ());
317 class DigestClient : IAuthenticationModule
320 static Hashtable cache; // cache entries by nonce
322 static DigestClient ()
324 cache = Hashtable.Synchronized (new Hashtable ());
327 public DigestClient () {}
329 // IAuthenticationModule
331 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials)
333 if (credentials == null || challenge == null)
336 string header = challenge.Trim ();
337 if (!header.ToLower ().StartsWith ("digest "))
340 HttpWebRequest request = webRequest as HttpWebRequest;
344 DigestSession ds = (DigestSession) cache [request.Address];
345 bool addDS = (ds == null);
347 ds = new DigestSession ();
349 if (!ds.Parse (challenge))
353 cache.Add (request.Address, ds);
355 return ds.Authenticate (webRequest, credentials);
358 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials)
360 HttpWebRequest request = webRequest as HttpWebRequest;
364 // check cache for URI
365 DigestSession ds = (DigestSession) cache [request.Address];
369 return ds.Authenticate (webRequest, credentials);
372 public string AuthenticationType {
373 get { return "Digest"; }
376 public bool CanPreAuthenticate {