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 string realm, opaque, nonce, algorithm;
43 static string [] keywords = { "realm", "opaque", "nonce", "algorithm", "qop" };
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 if (pos + 1 >= length || header [pos++] != '"')
150 pos = header.IndexOf ('"', pos);
154 value = header.Substring (beginQ, pos - beginQ);
162 static RandomNumberGenerator rng;
164 static DigestSession ()
166 rng = RandomNumberGenerator.Create ();
170 private HashAlgorithm hash;
171 private DigestHeaderParser parser;
172 private string _cnonce;
174 public DigestSession ()
179 public string Algorithm {
180 get { return parser.Algorithm; }
183 public string Realm {
184 get { return parser.Realm; }
187 public string Nonce {
188 get { return parser.Nonce; }
191 public string Opaque {
192 get { return parser.Opaque; }
196 get { return parser.QOP; }
199 public string CNonce {
201 if (_cnonce == null) {
202 // 15 is a multiple of 3 which is better for base64 because it
203 // wont end with '=' and risk messing up the server parsing
204 byte[] bincnonce = new byte [15];
205 rng.GetBytes (bincnonce);
206 _cnonce = Convert.ToBase64String (bincnonce);
207 Array.Clear (bincnonce, 0, bincnonce.Length);
213 public bool Parse (string challenge)
215 parser = new DigestHeaderParser (challenge);
216 if (!parser.Parse ()) {
217 Console.WriteLine ("Parser");
221 // build the hash object (only MD5 is defined in RFC2617)
222 if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
223 hash = HashAlgorithm.Create ("MD5");
228 private string HashToHexString (string toBeHashed)
234 byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
236 StringBuilder sb = new StringBuilder ();
237 foreach (byte b in result)
238 sb.Append (b.ToString ("x2"));
239 return sb.ToString ();
242 private string HA1 (string username, string password)
244 string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
245 if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
246 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
247 return HashToHexString (ha1);
250 private string HA2 (HttpWebRequest webRequest)
252 string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
253 if (QOP == "auth-int") {
255 // ha2 += String.Format (":{0}", hentity);
257 return HashToHexString (ha2);
260 private string Response (string username, string password, HttpWebRequest webRequest)
262 string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
264 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
265 response += HA2 (webRequest);
266 return HashToHexString (response);
269 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials)
272 throw new InvalidOperationException ();
274 HttpWebRequest request = webRequest as HttpWebRequest;
278 NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
279 string userName = cred.UserName;
280 if (userName == null || userName == "")
283 string password = cred.Password;
285 StringBuilder auth = new StringBuilder ();
286 auth.AppendFormat ("Digest username=\"{0}\", ", userName);
287 auth.AppendFormat ("realm=\"{0}\", ", Realm);
288 auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
289 auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
291 if (QOP != null) // quality of protection (server decision)
292 auth.AppendFormat ("qop=\"{0}\", ", QOP);
294 if (Algorithm != null) // hash algorithm (only MD5 in RFC2617)
295 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
298 // _nc MUST NOT change from here...
299 // number of request using this nonce
301 auth.AppendFormat ("nc={0:X8}, ", _nc);
304 // until here, now _nc can change
307 if (QOP != null) // opaque value from the client
308 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
310 if (Opaque != null) // exact same opaque value as received from server
311 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
313 auth.AppendFormat ("response=\"{0}\"", Response (userName, password, request));
314 return new Authorization (auth.ToString ());
318 class DigestClient : IAuthenticationModule
321 static Hashtable cache; // cache entries by nonce
323 static DigestClient ()
325 cache = Hashtable.Synchronized (new Hashtable ());
328 public DigestClient () {}
330 // IAuthenticationModule
332 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials)
334 if (credentials == null || challenge == null)
337 string header = challenge.Trim ();
338 if (!header.ToLower ().StartsWith ("digest "))
341 HttpWebRequest request = webRequest as HttpWebRequest;
345 DigestSession ds = (DigestSession) cache [request.Address];
346 bool addDS = (ds == null);
348 ds = new DigestSession ();
350 if (!ds.Parse (challenge))
354 cache.Add (request.Address, ds);
356 return ds.Authenticate (webRequest, credentials);
359 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials)
361 HttpWebRequest request = webRequest as HttpWebRequest;
365 // check cache for URI
366 DigestSession ds = (DigestSession) cache [request.Address];
370 return ds.Authenticate (webRequest, credentials);
373 public string AuthenticationType {
374 get { return "Digest"; }
377 public bool CanPreAuthenticate {