2006-01-04 Chris Toshok <toshok@ximian.com>
[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 #if NET_2_0
79                 [Obsolete]
80 #endif
81                 public FormsAuthentication ()
82                 {
83                 }
84
85                 public static bool Authenticate (string name, string password)
86                 {
87                         if (name == null || password == null)
88                                 return false;
89
90                         Initialize ();
91                         HttpContext context = HttpContext.Current;
92                         if (context == null)
93                                 throw new HttpException ("Context is null!");
94
95 #if CONFIGURATION_2_0
96                         AuthenticationSection section = (AuthenticationSection) WebConfigurationManager.GetWebApplicationSection (authConfigPath);
97                         FormsAuthenticationCredentials config = section.Forms.Credentials;
98                         FormsAuthenticationUser user = config.Users[name];
99                         string stored = null;
100
101                         if (user != null)
102                                 stored = user.Password;
103 #else
104                         AuthConfig config = context.GetConfig (authConfigPath) as AuthConfig;
105                         Hashtable users = config.CredentialUsers;
106                         string stored = users [name] as string;
107 #endif
108                         if (stored == null)
109                                 return false;
110
111                         switch (config.PasswordFormat) {
112                         case FormsAuthPasswordFormat.Clear:
113                                 /* Do nothing */
114                                 break;
115                         case FormsAuthPasswordFormat.MD5:
116                                 password = HashPasswordForStoringInConfigFile (password, "MD5");
117                                 break;
118                         case FormsAuthPasswordFormat.SHA1:
119                                 password = HashPasswordForStoringInConfigFile (password, "SHA1");
120                                 break;
121                         }
122
123                         return (password == stored);
124                 }
125
126                 static FormsAuthenticationTicket Decrypt2 (byte [] bytes)
127                 {
128                         if (protection == FormsProtectionEnum.None)
129                                 return FormsAuthenticationTicket.FromByteArray (bytes);
130
131 #if CONFIGURATION_2_0
132                         MachineKeySection config = (MachineKeySection) WebConfigurationManager.GetWebApplicationSection (machineKeyConfigPath);
133 #else
134                         MachineKeyConfig config = HttpContext.GetAppConfig (machineKeyConfigPath) as MachineKeyConfig;
135 #endif
136                         bool all = (protection == FormsProtectionEnum.All);
137
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);
143                                 bytes = null;
144                         }
145
146                         if (all || protection == FormsProtectionEnum.Validation) {
147                                 int count;
148                                 MachineKeyValidation validationType;
149
150 #if CONFIGURATION_2_0
151                                 validationType = config.Validation;
152 #else
153                                 validationType = config.ValidationType;
154 #endif
155                                 if (validationType == MachineKeyValidation.MD5)
156                                         count = MD5_hash_size;
157                                 else
158                                         count = SHA1_hash_size; // 3DES and SHA1
159
160 #if CONFIGURATION_2_0
161                                 byte [] vk = config.ValidationKeyBytes;
162 #else
163                                 byte [] vk = config.ValidationKey;
164 #endif
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);
168
169                                 byte [] hash = null;
170                                 switch (validationType) {
171                                 case MachineKeyValidation.MD5:
172                                         hash = MD5.Create ().ComputeHash (mix);
173                                         break;
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);
178                                         break;
179                                 }
180
181                                 if (result.Length < count)
182                                         throw new ArgumentException ("Error validating ticket (length).", "encryptedTicket");
183
184                                 int i, k;
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");
188                                 }
189                         }
190
191                         return FormsAuthenticationTicket.FromByteArray (result);
192                 }
193
194                 public static FormsAuthenticationTicket Decrypt (string encryptedTicket)
195                 {
196                         if (encryptedTicket == null || encryptedTicket == String.Empty)
197                                 throw new ArgumentException ("Invalid encrypted ticket", "encryptedTicket");
198
199                         Initialize ();
200
201                         FormsAuthenticationTicket ticket;
202 #if CONFIGURATION_2_0
203                         byte [] bytes = MachineKeySection.GetBytes (encryptedTicket, encryptedTicket.Length);
204 #else
205                         byte [] bytes = MachineKeyConfig.GetBytes (encryptedTicket, encryptedTicket.Length);
206 #endif
207                         try {
208                                 ticket = Decrypt2 (bytes);
209                         } catch (Exception) {
210                                 ticket = null;
211                         }
212
213                         return ticket;
214                 }
215
216                 public static string Encrypt (FormsAuthenticationTicket ticket)
217                 {
218                         if (ticket == null)
219                                 throw new ArgumentNullException ("ticket");
220
221                         Initialize ();
222                         byte [] ticket_bytes = ticket.ToByteArray ();
223                         if (protection == FormsProtectionEnum.None)
224                                 return GetHexString (ticket_bytes);
225
226                         byte [] result = ticket_bytes;
227 #if CONFIGURATION_2_0
228                         MachineKeySection config = (MachineKeySection) WebConfigurationManager.GetWebApplicationSection (machineKeyConfigPath);
229 #else
230                         MachineKeyConfig config = HttpContext.GetAppConfig (machineKeyConfigPath) as MachineKeyConfig;
231 #endif
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;
237 #else
238                                 byte [] vk = config.ValidationKey;
239 #endif
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);
243
244                                 switch (
245 #if CONFIGURATION_2_0
246                                         config.Validation
247 #else
248                                         config.ValidationType
249 #endif
250                                         ) {
251                                 case MachineKeyValidation.MD5:
252                                         valid_bytes = MD5.Create ().ComputeHash (mix);
253                                         break;
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);
258                                         break;
259                                 }
260
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);
266                         }
267
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);
272                         }
273
274                         return GetHexString (result);
275                 }
276
277                 public static HttpCookie GetAuthCookie (string userName, bool createPersistentCookie)
278                 {
279                         return GetAuthCookie (userName, createPersistentCookie, null);
280                 }
281
282                 public static HttpCookie GetAuthCookie (string userName, bool createPersistentCookie, string strCookiePath)
283                 {
284                         Initialize ();
285
286                         if (userName == null)
287                                 userName = String.Empty;
288
289                         if (strCookiePath == null || strCookiePath.Length == 0)
290                                 strCookiePath = cookiePath;
291
292                         DateTime now = DateTime.Now;
293                         DateTime then;
294                         if (createPersistentCookie)
295                                 then = now.AddYears (50);
296                         else
297                                 then = now.AddMinutes (timeout);
298
299                         FormsAuthenticationTicket ticket = new FormsAuthenticationTicket (1,
300                                                                                           userName,
301                                                                                           now,
302                                                                                           then,
303                                                                                           createPersistentCookie,
304                                                                                           String.Empty,
305                                                                                           cookiePath);
306
307                         if (!createPersistentCookie)
308                                 then = DateTime.MinValue;
309
310                         return new HttpCookie (cookieName, Encrypt (ticket), strCookiePath, then);
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 CONFIGURATION_2_0
382                                 AuthenticationSection section = (AuthenticationSection)WebConfigurationManager.GetWebApplicationSection (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 = config.DefaultUrl;
395                                 enable_crossapp_redirects = config.EnableCrossAppRedirects;
396                                 login_url = config.LoginUrl;
397 #else
398                                 HttpContext context = HttpContext.Current;
399 #if NET_2_0
400                                 AuthConfig authConfig = null;
401                                 if (context != null)
402                                         authConfig = context.GetConfig (authConfigPath) as AuthConfig;
403 #else
404                                 AuthConfig authConfig = context.GetConfig (authConfigPath) as AuthConfig;
405 #endif
406                                 if (authConfig != null) {
407                                         cookieName = authConfig.CookieName;
408                                         timeout = authConfig.Timeout;
409                                         cookiePath = authConfig.CookiePath;
410                                         protection = authConfig.Protection;
411 #if NET_1_1
412                                         requireSSL = authConfig.RequireSSL;
413                                         slidingExpiration = authConfig.SlidingExpiration;
414 #endif
415 #if NET_2_0
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;
422 #endif
423                                 } else {
424                                         cookieName = ".MONOAUTH";
425                                         timeout = 30;
426                                         cookiePath = "/";
427                                         protection = FormsProtectionEnum.All;
428 #if NET_1_1
429                                         slidingExpiration = true;
430 #endif
431 #if NET_2_0
432                                         cookie_domain = String.Empty;
433                                         cookie_mode = HttpCookieMode.UseDeviceProfile;
434                                         cookies_supported = true;
435                                         default_url = "/default.aspx";
436                                         login_url = "/login.aspx";
437 #endif
438                                 }
439 #endif
440
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++) {
445                                         if (i >= len)
446                                                 break;
447
448                                         init_vector [i] = (byte) cookieName [i];
449                                 }
450
451                                 initialized = true;
452                         }
453                 }
454
455                 public static void RedirectFromLoginPage (string userName, bool createPersistentCookie)
456                 {
457                         RedirectFromLoginPage (userName, createPersistentCookie, null);
458                 }
459
460                 public static void RedirectFromLoginPage (string userName, bool createPersistentCookie, string strCookiePath)
461                 {
462                         if (userName == null)
463                                 return;
464
465                         Initialize ();
466                         SetAuthCookie (userName, createPersistentCookie, strCookiePath);
467                         Redirect (GetRedirectUrl (userName, createPersistentCookie), false);
468                 }
469
470                 public static FormsAuthenticationTicket RenewTicketIfOld (FormsAuthenticationTicket tOld)
471                 {
472                         if (tOld == null)
473                                 return null;
474
475                         DateTime now = DateTime.Now;
476                         TimeSpan toIssue = now - tOld.IssueDate;
477                         TimeSpan toExpiration = tOld.Expiration - now;
478                         if (toExpiration > toIssue)
479                                 return tOld;
480
481                         FormsAuthenticationTicket tNew = tOld.Clone ();
482                         tNew.SetDates (now, now + (tOld.Expiration - tOld.IssueDate));
483                         return tNew;
484                 }
485
486                 public static void SetAuthCookie (string userName, bool createPersistentCookie)
487                 {
488                         Initialize ();
489                         SetAuthCookie (userName, createPersistentCookie, cookiePath);
490                 }
491
492                 public static void SetAuthCookie (string userName, bool createPersistentCookie, string strCookiePath)
493                 {
494                         HttpContext context = HttpContext.Current;
495                         if (context == null)
496                                 throw new HttpException ("Context is null!");
497
498                         HttpResponse response = context.Response;
499                         if (response == null)
500                                 throw new HttpException ("Response is null!");
501
502                         response.Cookies.Add (GetAuthCookie (userName, createPersistentCookie, strCookiePath));
503                 }
504
505                 public static void SignOut ()
506                 {
507                         Initialize ();
508
509                         HttpContext context = HttpContext.Current;
510                         if (context == null)
511                                 throw new HttpException ("Context is null!");
512
513                         HttpResponse response = context.Response;
514                         if (response == null)
515                                 throw new HttpException ("Response is null!");
516
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);
523                 }
524
525                 public static string FormsCookieName
526                 {
527                         get {
528                                 Initialize ();
529                                 return cookieName;
530                         }
531                 }
532
533                 public static string FormsCookiePath
534                 {
535                         get {
536                                 Initialize ();
537                                 return cookiePath;
538                         }
539                 }
540 #if NET_1_1
541                 public static bool RequireSSL {
542                         get {
543                                 Initialize ();
544                                 return requireSSL;
545                         }
546                 }
547
548                 public static bool SlidingExpiration {
549                         get {
550                                 Initialize ();
551                                 return slidingExpiration;
552                         }
553                 }
554 #endif
555
556 #if NET_2_0
557                 public static string CookieDomain {
558                         get { return cookie_domain; }
559                 }
560
561                 public static HttpCookieMode CookieMode {
562                         get { return cookie_mode; }
563                 }
564
565                 public static bool CookiesSupported {
566                         get { return cookies_supported; }
567                 }
568
569                 public static string DefaultUrl {
570                         get { return default_url; }
571                 }
572
573                 public static bool EnableCrossAppRedirects {
574                         get { return enable_crossapp_redirects; }
575                 }
576
577                 public static string LoginUrl {
578                         get { return login_url; }
579                 }
580
581                 public static void RedirectToLoginPage ()
582                 {
583                         Redirect (LoginUrl);
584                 }
585
586                 [MonoTODO ("needs more tests")]
587                 public static void RedirectToLoginPage (string extraQueryString)
588                 {
589                         // TODO: if ? is in LoginUrl (legal?), ? in query (legal?) ...
590                         Redirect (LoginUrl + "?" + extraQueryString);
591                 }
592 #endif
593                 private static void Redirect (string url)
594                 {
595                         HttpContext.Current.Response.Redirect (url);
596                 }
597
598                 private static void Redirect (string url, bool end)
599                 {
600                         HttpContext.Current.Response.Redirect (url, end);
601                 }
602         }
603 }