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 // 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:
24 // The above copyright notice and this permission notice shall be
25 // included in all copies or substantial portions of the Software.
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.
37 using System
.Collections
.Specialized
;
38 using System
.Configuration
;
40 using System
.Security
.Cryptography
;
41 using System
.Security
.Principal
;
46 namespace Mono
.Http
.Modules
48 public class DigestAuthenticationModule
: AuthenticationModule
50 // TODO: Digest.Nonce.Lifetime="0" Never expires
51 static int nonceLifetime
= 60;
52 static char[] trim
= {'='}
;
54 public DigestAuthenticationModule () : base ("Digest") {}
56 protected virtual bool IsValidNonce (string nonce
)
60 // pad nonce on the right with '=' until length is a multiple of 4
61 int numPadChars
= nonce
.Length
% 4;
63 numPadChars
= 4 - numPadChars
;
64 string newNonce
= nonce
.PadRight(nonce
.Length
+ numPadChars
, '=');
67 byte[] decodedBytes
= Convert
.FromBase64String(newNonce
);
68 string expireStr
= new ASCIIEncoding().GetString(decodedBytes
);
69 expireTime
= DateTime
.Parse(expireStr
);
71 catch (FormatException e
) {
75 return (DateTime
.Now
<= expireTime
);
78 protected virtual bool GetUserByName (HttpApplication app
, string username
,
79 out string password
, out string[] roles
)
81 password
= String
.Empty
;
82 roles
= new string[0];
84 string userFileName
= app
.Request
.MapPath (ConfigurationSettings
.AppSettings
["Digest.Users"]);
85 if (userFileName
== null || !File
.Exists (userFileName
))
88 XmlDocument userDoc
= new XmlDocument ();
89 userDoc
.Load (userFileName
);
91 string xPath
= String
.Format ("/users/user[@name='{0}']", username
);
92 XmlNode user
= userDoc
.SelectSingleNode (xPath
);
97 password
= user
.Attributes
["password"].Value
;
99 XmlNodeList roleNodes
= user
.SelectNodes ("role");
100 roles
= new string [roleNodes
.Count
];
102 foreach (XmlNode xn
in roleNodes
)
103 roles
[i
++] = xn
.Attributes
["name"].Value
;
108 protected override bool AcceptCredentials (HttpApplication app
, string authentication
)
111 ListDictionary reqInfo
= new ListDictionary ();
113 string[] elems
= authentication
.Split( new char[] {','}
);
114 foreach (string elem
in elems
) {
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
);
122 string username
= (string) reqInfo
["username"];
126 if (!GetUserByName (app
, username
, out password
, out roles
))
129 string realm
= ConfigurationSettings
.AppSettings
["Digest.Realm"];
131 // calculate the Digest hashes
133 // A1 = unq(username-value) ":" unq(realm-value) ":" passwd
134 string A1
= String
.Format ("{0}:{1}:{2}", username
, realm
, password
);
137 string HA1
= GetMD5HashBinHex (A1
);
139 // A2 = Method ":" digest-uri-value
140 string A2
= String
.Format ("{0}:{1}", app
.Request
.HttpMethod
, (string)reqInfo
["uri"]);
143 string HA2
= GetMD5HashBinHex(A2
);
145 // KD(secret, data) = H(concat(secret, ":", data))
147 // request-digest = <"> < KD ( H(A1), unq(nonce-value)
149 // ":" unq(cnonce-value)
150 // ":" unq(qop-value)
153 // if qop is missing,
154 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
156 string unhashedDigest
;
157 if (reqInfo
["qop"] != null) {
158 unhashedDigest
= String
.Format("{0}:{1}:{2}:{3}:{4}:{5}",
160 (string)reqInfo
["nonce"],
161 (string)reqInfo
["nc"],
162 (string)reqInfo
["cnonce"],
163 (string)reqInfo
["qop"],
167 unhashedDigest
= String
.Format("{0}:{1}:{2}",
169 (string)reqInfo
["nonce"],
173 string hashedDigest
= GetMD5HashBinHex (unhashedDigest
);
175 bool isNonceStale
= !IsValidNonce((string)reqInfo
["nonce"]);
176 app
.Context
.Items
["staleNonce"] = isNonceStale
;
178 bool result
= (((string)reqInfo
["response"] == hashedDigest
) && (!isNonceStale
));
180 IIdentity id
= new GenericIdentity (username
, AuthenticationMethod
);
181 app
.Context
.User
= new GenericPrincipal (id
, roles
);
186 #region Event Handlers
188 public override void OnEndRequest(object source
, EventArgs eventArgs
)
190 // We add the WWW-Authenticate header here, so if an authorization
191 // fails elsewhere than in this module, we can still request authentication
194 HttpApplication app
= (HttpApplication
) source
;
195 if (app
.Response
.StatusCode
!= 401 || !AuthenticationRequired
)
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
;
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\"");
217 app
.Response
.AppendHeader("WWW-Authenticate", challenge
.ToString());
218 app
.Response
.StatusCode
= 401;
223 private string GetMD5HashBinHex (string toBeHashed
)
225 MD5 hash
= MD5
.Create ();
226 byte[] result
= hash
.ComputeHash (Encoding
.ASCII
.GetBytes (toBeHashed
));
228 StringBuilder sb
= new StringBuilder ();
229 foreach (byte b
in result
)
230 sb
.Append (b
.ToString ("x2"));
231 return sb
.ToString ();
234 protected virtual string GetCurrentNonce ()
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
);