2 // System.Web.Security.FormsAuthentication
5 // Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 // (C) 2002,2003 Ximian, Inc (http://www.ximian.com)
8 // Copyright (c) 2005 Novell, Inc (http://www.novell.com)
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 using System.Collections;
34 using System.Security.Cryptography;
35 using System.Security.Permissions;
38 using System.Web.Configuration;
39 using System.Web.Util;
41 namespace System.Web.Security
43 // CAS - no InheritanceDemand here as the class is sealed
44 [AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
45 public sealed class FormsAuthentication
47 const int MD5_hash_size = 16;
48 const int SHA1_hash_size = 20;
50 static string authConfigPath = "system.web/authentication";
51 static string machineKeyConfigPath = "system.web/machineKey";
52 static bool initialized;
53 static string cookieName;
54 static string cookiePath;
56 static FormsProtectionEnum protection;
57 static object locker = new object ();
58 static byte [] init_vector; // initialization vector used for 3DES
60 static bool requireSSL;
61 static bool slidingExpiration;
64 static string cookie_domain;
65 static HttpCookieMode cookie_mode;
66 static bool cookies_supported;
67 static string default_url;
68 static bool enable_crossapp_redirects;
69 static string login_url;
71 // same names and order used in xsp
72 static string [] indexFiles = { "index.aspx",
81 public FormsAuthentication ()
85 public static bool Authenticate (string name, string password)
87 if (name == null || password == null)
91 HttpContext context = HttpContext.Current;
93 throw new HttpException ("Context is null!");
96 AuthenticationSection section = (AuthenticationSection) WebConfigurationManager.GetWebApplicationSection (authConfigPath);
97 FormsAuthenticationCredentials config = section.Forms.Credentials;
98 FormsAuthenticationUser user = config.Users[name];
102 stored = user.Password;
104 AuthConfig config = context.GetConfig (authConfigPath) as AuthConfig;
105 Hashtable users = config.CredentialUsers;
106 string stored = users [name] as string;
111 switch (config.PasswordFormat) {
112 case FormsAuthPasswordFormat.Clear:
115 case FormsAuthPasswordFormat.MD5:
116 password = HashPasswordForStoringInConfigFile (password, "MD5");
118 case FormsAuthPasswordFormat.SHA1:
119 password = HashPasswordForStoringInConfigFile (password, "SHA1");
123 return (password == stored);
126 static FormsAuthenticationTicket Decrypt2 (byte [] bytes)
128 if (protection == FormsProtectionEnum.None)
129 return FormsAuthenticationTicket.FromByteArray (bytes);
131 #if CONFIGURATION_2_0
132 MachineKeySection config = (MachineKeySection) WebConfigurationManager.GetWebApplicationSection (machineKeyConfigPath);
134 MachineKeyConfig config = HttpContext.GetAppConfig (machineKeyConfigPath) as MachineKeyConfig;
136 bool all = (protection == FormsProtectionEnum.All);
138 byte [] result = bytes;
139 if (all || protection == FormsProtectionEnum.Encryption) {
140 ICryptoTransform decryptor;
141 decryptor = TripleDES.Create ().CreateDecryptor (config.DecryptionKey192Bits, init_vector);
142 result = decryptor.TransformFinalBlock (bytes, 0, bytes.Length);
146 if (all || protection == FormsProtectionEnum.Validation) {
148 MachineKeyValidation validationType;
150 #if CONFIGURATION_2_0
151 validationType = config.Validation;
153 validationType = config.ValidationType;
155 if (validationType == MachineKeyValidation.MD5)
156 count = MD5_hash_size;
158 count = SHA1_hash_size; // 3DES and SHA1
160 #if CONFIGURATION_2_0
161 byte [] vk = config.ValidationKeyBytes;
163 byte [] vk = config.ValidationKey;
165 byte [] mix = new byte [result.Length - count + vk.Length];
166 Buffer.BlockCopy (result, 0, mix, 0, result.Length - count);
167 Buffer.BlockCopy (vk, 0, mix, result.Length - count, vk.Length);
170 switch (validationType) {
171 case MachineKeyValidation.MD5:
172 hash = MD5.Create ().ComputeHash (mix);
174 // From MS docs: "When 3DES is specified, forms authentication defaults to SHA1"
175 case MachineKeyValidation.TripleDES:
176 case MachineKeyValidation.SHA1:
177 hash = SHA1.Create ().ComputeHash (mix);
181 if (result.Length < count)
182 throw new ArgumentException ("Error validating ticket (length).", "encryptedTicket");
185 for (i = result.Length - count, k = 0; k < count; i++, k++) {
186 if (result [i] != hash [k])
187 throw new ArgumentException ("Error validating ticket.", "encryptedTicket");
191 return FormsAuthenticationTicket.FromByteArray (result);
194 public static FormsAuthenticationTicket Decrypt (string encryptedTicket)
196 if (encryptedTicket == null || encryptedTicket == String.Empty)
197 throw new ArgumentException ("Invalid encrypted ticket", "encryptedTicket");
201 FormsAuthenticationTicket ticket;
202 #if CONFIGURATION_2_0
203 byte [] bytes = MachineKeySection.GetBytes (encryptedTicket, encryptedTicket.Length);
205 byte [] bytes = MachineKeyConfig.GetBytes (encryptedTicket, encryptedTicket.Length);
208 ticket = Decrypt2 (bytes);
209 } catch (Exception) {
216 public static string Encrypt (FormsAuthenticationTicket ticket)
219 throw new ArgumentNullException ("ticket");
222 byte [] ticket_bytes = ticket.ToByteArray ();
223 if (protection == FormsProtectionEnum.None)
224 return GetHexString (ticket_bytes);
226 byte [] result = ticket_bytes;
227 #if CONFIGURATION_2_0
228 MachineKeySection config = (MachineKeySection) WebConfigurationManager.GetWebApplicationSection (machineKeyConfigPath);
230 MachineKeyConfig config = HttpContext.GetAppConfig (machineKeyConfigPath) as MachineKeyConfig;
232 bool all = (protection == FormsProtectionEnum.All);
233 if (all || protection == FormsProtectionEnum.Validation) {
234 byte [] valid_bytes = null;
235 #if CONFIGURATION_2_0
236 byte [] vk = config.ValidationKeyBytes;
238 byte [] vk = config.ValidationKey;
240 byte [] mix = new byte [ticket_bytes.Length + vk.Length];
241 Buffer.BlockCopy (ticket_bytes, 0, mix, 0, ticket_bytes.Length);
242 Buffer.BlockCopy (vk, 0, mix, result.Length, vk.Length);
245 #if CONFIGURATION_2_0
248 config.ValidationType
251 case MachineKeyValidation.MD5:
252 valid_bytes = MD5.Create ().ComputeHash (mix);
254 // From MS docs: "When 3DES is specified, forms authentication defaults to SHA1"
255 case MachineKeyValidation.TripleDES:
256 case MachineKeyValidation.SHA1:
257 valid_bytes = SHA1.Create ().ComputeHash (mix);
261 int tlen = ticket_bytes.Length;
262 int vlen = valid_bytes.Length;
263 result = new byte [tlen + vlen];
264 Buffer.BlockCopy (ticket_bytes, 0, result, 0, tlen);
265 Buffer.BlockCopy (valid_bytes, 0, result, tlen, vlen);
268 if (all || protection == FormsProtectionEnum.Encryption) {
269 ICryptoTransform encryptor;
270 encryptor = TripleDES.Create ().CreateEncryptor (config.DecryptionKey192Bits, init_vector);
271 result = encryptor.TransformFinalBlock (result, 0, result.Length);
274 return GetHexString (result);
277 public static HttpCookie GetAuthCookie (string userName, bool createPersistentCookie)
279 return GetAuthCookie (userName, createPersistentCookie, null);
282 public static HttpCookie GetAuthCookie (string userName, bool createPersistentCookie, string strCookiePath)
286 if (userName == null)
287 userName = String.Empty;
289 if (strCookiePath == null || strCookiePath.Length == 0)
290 strCookiePath = cookiePath;
292 DateTime now = DateTime.Now;
294 if (createPersistentCookie)
295 then = now.AddYears (50);
297 then = now.AddMinutes (timeout);
299 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket (1,
303 createPersistentCookie,
307 if (!createPersistentCookie)
308 then = DateTime.MinValue;
310 return new HttpCookie (cookieName, Encrypt (ticket), strCookiePath, then);
313 public static string GetRedirectUrl (string userName, bool createPersistentCookie)
315 if (userName == null)
319 HttpRequest request = HttpContext.Current.Request;
320 string returnUrl = request ["RETURNURL"];
321 if (returnUrl != null)
324 returnUrl = request.ApplicationPath;
325 string apppath = request.PhysicalApplicationPath;
328 foreach (string indexFile in indexFiles) {
329 string filePath = Path.Combine (apppath, indexFile);
330 if (File.Exists (filePath)) {
331 returnUrl = UrlUtils.Combine (returnUrl, indexFile);
338 returnUrl = UrlUtils.Combine (returnUrl, "index.aspx");
343 static string GetHexString (byte [] bytes)
345 StringBuilder result = new StringBuilder (bytes.Length * 2);
346 foreach (byte b in bytes)
347 result.AppendFormat ("{0:X2}", (int) b);
349 return result.ToString ();
352 public static string HashPasswordForStoringInConfigFile (string password, string passwordFormat)
354 if (password == null)
355 throw new ArgumentNullException ("password");
357 if (passwordFormat == null)
358 throw new ArgumentNullException ("passwordFormat");
361 if (String.Compare (passwordFormat, "MD5", true) == 0) {
362 bytes = MD5.Create ().ComputeHash (Encoding.UTF8.GetBytes (password));
363 } else if (String.Compare (passwordFormat, "SHA1", true) == 0) {
364 bytes = SHA1.Create ().ComputeHash (Encoding.UTF8.GetBytes (password));
366 throw new ArgumentException ("The format must be either MD5 or SHA1", "passwordFormat");
369 return GetHexString (bytes);
372 public static void Initialize ()
381 #if CONFIGURATION_2_0
382 AuthenticationSection section = (AuthenticationSection)WebConfigurationManager.GetWebApplicationSection (authConfigPath);
383 FormsAuthenticationConfiguration config = section.Forms;
385 cookieName = config.Name;
386 timeout = (int)config.Timeout.TotalMinutes;
387 cookiePath = config.Path;
388 protection = config.Protection;
389 requireSSL = config.RequireSSL;
390 slidingExpiration = config.SlidingExpiration;
391 cookie_domain = config.Domain;
392 cookie_mode = config.Cookieless;
393 cookies_supported = true; /* XXX ? */
394 default_url = config.DefaultUrl;
395 enable_crossapp_redirects = config.EnableCrossAppRedirects;
396 login_url = config.LoginUrl;
398 HttpContext context = HttpContext.Current;
400 AuthConfig authConfig = null;
402 authConfig = context.GetConfig (authConfigPath) as AuthConfig;
404 AuthConfig authConfig = context.GetConfig (authConfigPath) as AuthConfig;
406 if (authConfig != null) {
407 cookieName = authConfig.CookieName;
408 timeout = authConfig.Timeout;
409 cookiePath = authConfig.CookiePath;
410 protection = authConfig.Protection;
412 requireSSL = authConfig.RequireSSL;
413 slidingExpiration = authConfig.SlidingExpiration;
416 cookie_domain = authConfig.CookieDomain;
417 cookie_mode = authConfig.CookieMode;
418 cookies_supported = authConfig.CookiesSupported;
419 default_url = authConfig.DefaultUrl;
420 enable_crossapp_redirects = authConfig.EnableCrossAppRedirects;
421 login_url = authConfig.LoginUrl;
424 cookieName = ".MONOAUTH";
427 protection = FormsProtectionEnum.All;
429 slidingExpiration = true;
432 cookie_domain = String.Empty;
433 cookie_mode = HttpCookieMode.UseDeviceProfile;
434 cookies_supported = true;
435 default_url = "/default.aspx";
436 login_url = "/login.aspx";
441 // IV is 8 bytes long for 3DES
442 init_vector = new byte [8];
443 int len = cookieName.Length;
444 for (int i = 0; i < 8; i++) {
448 init_vector [i] = (byte) cookieName [i];
455 public static void RedirectFromLoginPage (string userName, bool createPersistentCookie)
457 RedirectFromLoginPage (userName, createPersistentCookie, null);
460 public static void RedirectFromLoginPage (string userName, bool createPersistentCookie, string strCookiePath)
462 if (userName == null)
466 SetAuthCookie (userName, createPersistentCookie, strCookiePath);
467 Redirect (GetRedirectUrl (userName, createPersistentCookie), false);
470 public static FormsAuthenticationTicket RenewTicketIfOld (FormsAuthenticationTicket tOld)
475 DateTime now = DateTime.Now;
476 TimeSpan toIssue = now - tOld.IssueDate;
477 TimeSpan toExpiration = tOld.Expiration - now;
478 if (toExpiration > toIssue)
481 FormsAuthenticationTicket tNew = tOld.Clone ();
482 tNew.SetDates (now, now + (tOld.Expiration - tOld.IssueDate));
486 public static void SetAuthCookie (string userName, bool createPersistentCookie)
489 SetAuthCookie (userName, createPersistentCookie, cookiePath);
492 public static void SetAuthCookie (string userName, bool createPersistentCookie, string strCookiePath)
494 HttpContext context = HttpContext.Current;
496 throw new HttpException ("Context is null!");
498 HttpResponse response = context.Response;
499 if (response == null)
500 throw new HttpException ("Response is null!");
502 response.Cookies.Add (GetAuthCookie (userName, createPersistentCookie, strCookiePath));
505 public static void SignOut ()
509 HttpContext context = HttpContext.Current;
511 throw new HttpException ("Context is null!");
513 HttpResponse response = context.Response;
514 if (response == null)
515 throw new HttpException ("Response is null!");
517 HttpCookieCollection cc = response.Cookies;
518 cc.Remove (cookieName);
519 HttpCookie expiration_cookie = new HttpCookie (cookieName, "");
520 expiration_cookie.Expires = new DateTime (1999, 10, 12);
521 expiration_cookie.Path = cookiePath;
522 cc.Add (expiration_cookie);
525 public static string FormsCookieName
533 public static string FormsCookiePath
541 public static bool RequireSSL {
548 public static bool SlidingExpiration {
551 return slidingExpiration;
557 public static string CookieDomain {
558 get { return cookie_domain; }
561 public static HttpCookieMode CookieMode {
562 get { return cookie_mode; }
565 public static bool CookiesSupported {
566 get { return cookies_supported; }
569 public static string DefaultUrl {
570 get { return default_url; }
573 public static bool EnableCrossAppRedirects {
574 get { return enable_crossapp_redirects; }
577 public static string LoginUrl {
578 get { return login_url; }
581 public static void RedirectToLoginPage ()
586 [MonoTODO ("needs more tests")]
587 public static void RedirectToLoginPage (string extraQueryString)
589 // TODO: if ? is in LoginUrl (legal?), ? in query (legal?) ...
590 Redirect (LoginUrl + "?" + extraQueryString);
593 private static void Redirect (string url)
595 HttpContext.Current.Response.Redirect (url);
598 private static void Redirect (string url, bool end)
600 HttpContext.Current.Response.Redirect (url, end);