New test.
[mono.git] / mcs / class / System.Web / System.Web.Security / FormsAuthentication.cs
1 //
2 // System.Web.Security.FormsAuthentication
3 //
4 // Authors:
5 //      Gonzalo Paniagua Javier (gonzalo@ximian.com)
6 //
7 // (C) 2002,2003 Ximian, Inc (http://www.ximian.com)
8 // Copyright (c) 2005 Novell, Inc (http://www.novell.com)
9 //
10
11 //
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:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
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.
30 //
31
32 using System.Collections;
33 using System.IO;
34 using System.Security.Cryptography;
35 using System.Security.Permissions;
36 using System.Text;
37 using System.Web;
38 using System.Web.Configuration;
39 using System.Web.Util;
40
41 namespace System.Web.Security
42 {
43         // CAS - no InheritanceDemand here as the class is sealed
44         [AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
45         public sealed class FormsAuthentication
46         {
47                 const int MD5_hash_size = 16;
48                 const int SHA1_hash_size = 20;
49
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;
55                 static int timeout;
56                 static FormsProtectionEnum protection;
57                 static object locker = new object ();
58                 static byte [] init_vector; // initialization vector used for 3DES
59 #if NET_1_1
60                 static bool requireSSL;
61                 static bool slidingExpiration;
62 #endif
63 #if NET_2_0
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;
70 #endif
71                 // same names and order used in xsp
72                 static string [] indexFiles = { "index.aspx",
73                                                 "Default.aspx",
74                                                 "default.aspx",
75                                                 "index.html",
76                                                 "index.htm" };
77
78                 public FormsAuthentication ()
79                 {
80                 }
81
82                 public static bool Authenticate (string name, string password)
83                 {
84                         if (name == null || password == null)
85                                 return false;
86
87                         Initialize ();
88                         HttpContext context = HttpContext.Current;
89                         if (context == null)
90                                 throw new HttpException ("Context is null!");
91
92 #if NET_2_0
93                         AuthenticationSection section = (AuthenticationSection) WebConfigurationManager.GetSection (authConfigPath);
94                         FormsAuthenticationCredentials config = section.Forms.Credentials;
95                         FormsAuthenticationUser user = config.Users[name];
96                         string stored = null;
97
98                         if (user != null)
99                                 stored = user.Password;
100 #else
101                         AuthConfig config = context.GetConfig (authConfigPath) as AuthConfig;
102                         Hashtable users = config.CredentialUsers;
103                         string stored = users [name] as string;
104 #endif
105                         if (stored == null)
106                                 return false;
107
108                         switch (config.PasswordFormat) {
109                         case FormsAuthPasswordFormat.Clear:
110                                 /* Do nothing */
111                                 break;
112                         case FormsAuthPasswordFormat.MD5:
113                                 password = HashPasswordForStoringInConfigFile (password, "MD5");
114                                 break;
115                         case FormsAuthPasswordFormat.SHA1:
116                                 password = HashPasswordForStoringInConfigFile (password, "SHA1");
117                                 break;
118                         }
119
120                         return (password == stored);
121                 }
122
123                 static FormsAuthenticationTicket Decrypt2 (byte [] bytes)
124                 {
125                         if (protection == FormsProtectionEnum.None)
126                                 return FormsAuthenticationTicket.FromByteArray (bytes);
127
128 #if NET_2_0
129                         MachineKeySection config = (MachineKeySection) WebConfigurationManager.GetSection (machineKeyConfigPath);
130 #else
131                         MachineKeyConfig config = HttpContext.GetAppConfig (machineKeyConfigPath) as MachineKeyConfig;
132 #endif
133                         bool all = (protection == FormsProtectionEnum.All);
134
135                         byte [] result = bytes;
136                         if (all || protection == FormsProtectionEnum.Encryption) {
137                                 ICryptoTransform decryptor;
138                                 decryptor = TripleDES.Create ().CreateDecryptor (config.DecryptionKey192Bits, init_vector);
139                                 result = decryptor.TransformFinalBlock (bytes, 0, bytes.Length);
140                                 bytes = null;
141                         }
142
143                         if (all || protection == FormsProtectionEnum.Validation) {
144                                 int count;
145                                 MachineKeyValidation validationType;
146
147 #if NET_2_0
148                                 validationType = config.Validation;
149 #else
150                                 validationType = config.ValidationType;
151 #endif
152                                 if (validationType == MachineKeyValidation.MD5)
153                                         count = MD5_hash_size;
154                                 else
155                                         count = SHA1_hash_size; // 3DES and SHA1
156
157 #if NET_2_0
158                                 byte [] vk = config.ValidationKeyBytes;
159 #else
160                                 byte [] vk = config.ValidationKey;
161 #endif
162                                 byte [] mix = new byte [result.Length - count + vk.Length];
163                                 Buffer.BlockCopy (result, 0, mix, 0, result.Length - count);
164                                 Buffer.BlockCopy (vk, 0, mix, result.Length - count, vk.Length);
165
166                                 byte [] hash = null;
167                                 switch (validationType) {
168                                 case MachineKeyValidation.MD5:
169                                         hash = MD5.Create ().ComputeHash (mix);
170                                         break;
171                                 // From MS docs: "When 3DES is specified, forms authentication defaults to SHA1"
172                                 case MachineKeyValidation.TripleDES:
173                                 case MachineKeyValidation.SHA1:
174                                         hash = SHA1.Create ().ComputeHash (mix);
175                                         break;
176                                 }
177
178                                 if (result.Length < count)
179                                         throw new ArgumentException ("Error validating ticket (length).", "encryptedTicket");
180
181                                 int i, k;
182                                 for (i = result.Length - count, k = 0; k < count; i++, k++) {
183                                         if (result [i] != hash [k])
184                                                 throw new ArgumentException ("Error validating ticket.", "encryptedTicket");
185                                 }
186                         }
187
188                         return FormsAuthenticationTicket.FromByteArray (result);
189                 }
190
191                 public static FormsAuthenticationTicket Decrypt (string encryptedTicket)
192                 {
193                         if (encryptedTicket == null || encryptedTicket == String.Empty)
194                                 throw new ArgumentException ("Invalid encrypted ticket", "encryptedTicket");
195
196                         Initialize ();
197
198                         FormsAuthenticationTicket ticket;
199 #if NET_2_0
200                         byte [] bytes = MachineKeySection.GetBytes (encryptedTicket, encryptedTicket.Length);
201 #else
202                         byte [] bytes = MachineKeyConfig.GetBytes (encryptedTicket, encryptedTicket.Length);
203 #endif
204                         try {
205                                 ticket = Decrypt2 (bytes);
206                         } catch (Exception) {
207                                 ticket = null;
208                         }
209
210                         return ticket;
211                 }
212
213                 public static string Encrypt (FormsAuthenticationTicket ticket)
214                 {
215                         if (ticket == null)
216                                 throw new ArgumentNullException ("ticket");
217
218                         Initialize ();
219                         byte [] ticket_bytes = ticket.ToByteArray ();
220                         if (protection == FormsProtectionEnum.None)
221                                 return GetHexString (ticket_bytes);
222
223                         byte [] result = ticket_bytes;
224 #if NET_2_0
225                         MachineKeySection config = (MachineKeySection) WebConfigurationManager.GetSection (machineKeyConfigPath);
226 #else
227                         MachineKeyConfig config = HttpContext.GetAppConfig (machineKeyConfigPath) as MachineKeyConfig;
228 #endif
229                         bool all = (protection == FormsProtectionEnum.All);
230                         if (all || protection == FormsProtectionEnum.Validation) {
231                                 byte [] valid_bytes = null;
232 #if NET_2_0
233                                 byte [] vk = config.ValidationKeyBytes;
234 #else
235                                 byte [] vk = config.ValidationKey;
236 #endif
237                                 byte [] mix = new byte [ticket_bytes.Length + vk.Length];
238                                 Buffer.BlockCopy (ticket_bytes, 0, mix, 0, ticket_bytes.Length);
239                                 Buffer.BlockCopy (vk, 0, mix, result.Length, vk.Length);
240
241                                 switch (
242 #if NET_2_0
243                                         config.Validation
244 #else
245                                         config.ValidationType
246 #endif
247                                         ) {
248                                 case MachineKeyValidation.MD5:
249                                         valid_bytes = MD5.Create ().ComputeHash (mix);
250                                         break;
251                                 // From MS docs: "When 3DES is specified, forms authentication defaults to SHA1"
252                                 case MachineKeyValidation.TripleDES:
253                                 case MachineKeyValidation.SHA1:
254                                         valid_bytes = SHA1.Create ().ComputeHash (mix);
255                                         break;
256                                 }
257
258                                 int tlen = ticket_bytes.Length;
259                                 int vlen = valid_bytes.Length;
260                                 result = new byte [tlen + vlen];
261                                 Buffer.BlockCopy (ticket_bytes, 0, result, 0, tlen);
262                                 Buffer.BlockCopy (valid_bytes, 0, result, tlen, vlen);
263                         }
264
265                         if (all || protection == FormsProtectionEnum.Encryption) {
266                                 ICryptoTransform encryptor;
267                                 encryptor = TripleDES.Create ().CreateEncryptor (config.DecryptionKey192Bits, init_vector);
268                                 result = encryptor.TransformFinalBlock (result, 0, result.Length);
269                         }
270
271                         return GetHexString (result);
272                 }
273
274                 public static HttpCookie GetAuthCookie (string userName, bool createPersistentCookie)
275                 {
276                         return GetAuthCookie (userName, createPersistentCookie, null);
277                 }
278
279                 public static HttpCookie GetAuthCookie (string userName, bool createPersistentCookie, string strCookiePath)
280                 {
281                         Initialize ();
282
283                         if (userName == null)
284                                 userName = String.Empty;
285
286                         if (strCookiePath == null || strCookiePath.Length == 0)
287                                 strCookiePath = cookiePath;
288
289                         DateTime now = DateTime.Now;
290                         DateTime then;
291                         if (createPersistentCookie)
292                                 then = now.AddYears (50);
293                         else
294                                 then = now.AddMinutes (timeout);
295
296                         FormsAuthenticationTicket ticket = new FormsAuthenticationTicket (1,
297                                                                                           userName,
298                                                                                           now,
299                                                                                           then,
300                                                                                           createPersistentCookie,
301                                                                                           String.Empty,
302                                                                                           cookiePath);
303
304                         if (!createPersistentCookie)
305                                 then = DateTime.MinValue;
306
307                         HttpCookie cookie = new HttpCookie (cookieName, Encrypt (ticket), strCookiePath, then);
308                         if (requireSSL)
309                                 cookie.Secure = true;
310                         return cookie;
311                 }
312
313                 public static string GetRedirectUrl (string userName, bool createPersistentCookie)
314                 {
315                         if (userName == null)
316                                 return null;
317
318                         Initialize ();
319                         HttpRequest request = HttpContext.Current.Request;
320                         string returnUrl = request ["RETURNURL"];
321                         if (returnUrl != null)
322                                 return returnUrl;
323
324                         returnUrl = request.ApplicationPath;
325                         string apppath = request.PhysicalApplicationPath;
326                         bool found = false;
327
328                         foreach (string indexFile in indexFiles) {
329                                 string filePath = Path.Combine (apppath, indexFile);
330                                 if (File.Exists (filePath)) {
331                                         returnUrl = UrlUtils.Combine (returnUrl, indexFile);
332                                         found = true;
333                                         break;
334                                 }
335                         }
336
337                         if (!found)
338                                 returnUrl = UrlUtils.Combine (returnUrl, "index.aspx");
339
340                         return returnUrl;
341                 }
342
343                 static string GetHexString (byte [] bytes)
344                 {
345                         StringBuilder result = new StringBuilder (bytes.Length * 2);
346                         foreach (byte b in bytes)
347                                 result.AppendFormat ("{0:X2}", (int) b);
348
349                         return result.ToString ();
350                 }
351
352                 public static string HashPasswordForStoringInConfigFile (string password, string passwordFormat)
353                 {
354                         if (password == null)
355                                 throw new ArgumentNullException ("password");
356
357                         if (passwordFormat == null)
358                                 throw new ArgumentNullException ("passwordFormat");
359
360                         byte [] bytes;
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));
365                         } else {
366                                 throw new ArgumentException ("The format must be either MD5 or SHA1", "passwordFormat");
367                         }
368
369                         return GetHexString (bytes);
370                 }
371
372                 public static void Initialize ()
373                 {
374                         if (initialized)
375                                 return;
376
377                         lock (locker) {
378                                 if (initialized)
379                                         return;
380
381 #if NET_2_0
382                                 AuthenticationSection section = (AuthenticationSection)WebConfigurationManager.GetSection (authConfigPath);
383                                 FormsAuthenticationConfiguration config = section.Forms;
384
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 = MapUrl(config.DefaultUrl);
395                                 enable_crossapp_redirects = config.EnableCrossAppRedirects;
396                                 login_url = MapUrl(config.LoginUrl);
397 #else
398                                 HttpContext context = HttpContext.Current;
399                                 AuthConfig authConfig = context.GetConfig (authConfigPath) as AuthConfig;
400                                 if (authConfig != null) {
401                                         cookieName = authConfig.CookieName;
402                                         timeout = authConfig.Timeout;
403                                         cookiePath = authConfig.CookiePath;
404                                         protection = authConfig.Protection;
405 #if NET_1_1
406                                         requireSSL = authConfig.RequireSSL;
407                                         slidingExpiration = authConfig.SlidingExpiration;
408 #endif
409                                 } else {
410                                         cookieName = ".MONOAUTH";
411                                         timeout = 30;
412                                         cookiePath = "/";
413                                         protection = FormsProtectionEnum.All;
414 #if NET_1_1
415                                         slidingExpiration = true;
416 #endif
417                                 }
418 #endif
419
420                                 // IV is 8 bytes long for 3DES
421                                 init_vector = new byte [8];
422                                 int len = cookieName.Length;
423                                 for (int i = 0; i < 8; i++) {
424                                         if (i >= len)
425                                                 break;
426
427                                         init_vector [i] = (byte) cookieName [i];
428                                 }
429
430                                 initialized = true;
431                         }
432                 }
433
434                 static string MapUrl (string url) {
435                         if (UrlUtils.IsRelativeUrl (url))
436                                 return UrlUtils.Combine (HttpRuntime.AppDomainAppVirtualPath, url);
437                         else
438                                 return UrlUtils.ResolveVirtualPathFromAppAbsolute (url);
439                 }
440
441                 public static void RedirectFromLoginPage (string userName, bool createPersistentCookie)
442                 {
443                         RedirectFromLoginPage (userName, createPersistentCookie, null);
444                 }
445
446                 public static void RedirectFromLoginPage (string userName, bool createPersistentCookie, string strCookiePath)
447                 {
448                         if (userName == null)
449                                 return;
450
451                         Initialize ();
452                         SetAuthCookie (userName, createPersistentCookie, strCookiePath);
453                         Redirect (GetRedirectUrl (userName, createPersistentCookie), false);
454                 }
455
456                 public static FormsAuthenticationTicket RenewTicketIfOld (FormsAuthenticationTicket tOld)
457                 {
458                         if (tOld == null)
459                                 return null;
460
461                         DateTime now = DateTime.Now;
462                         TimeSpan toIssue = now - tOld.IssueDate;
463                         TimeSpan toExpiration = tOld.Expiration - now;
464                         if (toExpiration > toIssue)
465                                 return tOld;
466
467                         FormsAuthenticationTicket tNew = tOld.Clone ();
468                         tNew.SetDates (now, now + (tOld.Expiration - tOld.IssueDate));
469                         return tNew;
470                 }
471
472                 public static void SetAuthCookie (string userName, bool createPersistentCookie)
473                 {
474                         Initialize ();
475                         SetAuthCookie (userName, createPersistentCookie, cookiePath);
476                 }
477
478                 public static void SetAuthCookie (string userName, bool createPersistentCookie, string strCookiePath)
479                 {
480                         HttpContext context = HttpContext.Current;
481                         if (context == null)
482                                 throw new HttpException ("Context is null!");
483
484                         HttpResponse response = context.Response;
485                         if (response == null)
486                                 throw new HttpException ("Response is null!");
487
488                         response.Cookies.Add (GetAuthCookie (userName, createPersistentCookie, strCookiePath));
489                 }
490
491                 public static void SignOut ()
492                 {
493                         Initialize ();
494
495                         HttpContext context = HttpContext.Current;
496                         if (context == null)
497                                 throw new HttpException ("Context is null!");
498
499                         HttpResponse response = context.Response;
500                         if (response == null)
501                                 throw new HttpException ("Response is null!");
502
503                         HttpCookieCollection cc = response.Cookies;
504                         cc.Remove (cookieName);
505                         HttpCookie expiration_cookie = new HttpCookie (cookieName, "");
506                         expiration_cookie.Expires = new DateTime (1999, 10, 12);
507                         expiration_cookie.Path = cookiePath;
508                         cc.Add (expiration_cookie);
509                 }
510
511                 public static string FormsCookieName
512                 {
513                         get {
514                                 Initialize ();
515                                 return cookieName;
516                         }
517                 }
518
519                 public static string FormsCookiePath
520                 {
521                         get {
522                                 Initialize ();
523                                 return cookiePath;
524                         }
525                 }
526 #if NET_1_1
527                 public static bool RequireSSL {
528                         get {
529                                 Initialize ();
530                                 return requireSSL;
531                         }
532                 }
533
534                 public static bool SlidingExpiration {
535                         get {
536                                 Initialize ();
537                                 return slidingExpiration;
538                         }
539                 }
540 #endif
541
542 #if NET_2_0
543                 public static string CookieDomain {
544                         get { Initialize (); return cookie_domain; }
545                 }
546
547                 public static HttpCookieMode CookieMode {
548                         get { Initialize (); return cookie_mode; }
549                 }
550
551                 public static bool CookiesSupported {
552                         get { Initialize (); return cookies_supported; }
553                 }
554
555                 public static string DefaultUrl {
556                         get { Initialize (); return default_url; }
557                 }
558
559                 public static bool EnableCrossAppRedirects {
560                         get { Initialize (); return enable_crossapp_redirects; }
561                 }
562
563                 public static string LoginUrl {
564                         get { Initialize (); return login_url; }
565                 }
566
567                 public static void RedirectToLoginPage ()
568                 {
569                         Redirect (LoginUrl);
570                 }
571
572                 [MonoTODO ("needs more tests")]
573                 public static void RedirectToLoginPage (string extraQueryString)
574                 {
575                         // TODO: if ? is in LoginUrl (legal?), ? in query (legal?) ...
576                         Redirect (LoginUrl + "?" + extraQueryString);
577                 }
578 #endif
579                 private static void Redirect (string url)
580                 {
581                         HttpContext.Current.Response.Redirect (url);
582                 }
583
584                 private static void Redirect (string url, bool end)
585                 {
586                         HttpContext.Current.Response.Redirect (url, end);
587                 }
588         }
589 }