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";
53 const string Forms_initialized
= "Forms.initialized";
54 const string Forms_cookieName
= "Forms.cookieName";
55 const string Forms_cookiePath
= "Forms.cookiePath";
56 const string Forms_timeout
= "Forms.timeout";
57 const string Forms_protection
= "Forms.protection";
58 const string Forms_init_vector
= "Forms.init_vector";
59 static bool initialized
62 object o
= AppDomain
.CurrentDomain
.GetData (Forms_initialized
);
63 return o
!= null ? (bool) o
: false;
65 set { AppDomain.CurrentDomain.SetData (Forms_initialized, value); }
67 static string cookieName
69 get { return (string) AppDomain.CurrentDomain.GetData (Forms_cookieName); }
70 set { AppDomain.CurrentDomain.SetData (Forms_cookieName, value); }
72 static string cookiePath
74 get { return (string) AppDomain.CurrentDomain.GetData (Forms_cookiePath); }
75 set { AppDomain.CurrentDomain.SetData (Forms_cookiePath, value); }
80 object o
= AppDomain
.CurrentDomain
.GetData (Forms_timeout
);
81 return o
!= null ? (int) o
: 0;
83 set { AppDomain.CurrentDomain.SetData (Forms_timeout, value); }
85 static FormsProtectionEnum protection
87 get { return (FormsProtectionEnum) AppDomain.CurrentDomain.GetData (Forms_protection); }
88 set { AppDomain.CurrentDomain.SetData (Forms_protection, value); }
90 static byte [] init_vector
92 get { return (byte []) AppDomain.CurrentDomain.GetData (Forms_init_vector); }
93 set { AppDomain.CurrentDomain.SetData (Forms_init_vector, value); }
95 static object locker
= new object ();
97 static bool initialized
;
98 static string cookieName
;
99 static string cookiePath
;
101 static FormsProtectionEnum protection
;
102 static object locker
= new object ();
103 static byte [] init_vector
; // initialization vector used for 3DES
107 const string Forms_requireSSL
= "Forms.requireSSL";
108 const string Forms_slidingExpiration
= "Forms.slidingExpiration";
110 static bool requireSSL
113 object o
= AppDomain
.CurrentDomain
.GetData (Forms_requireSSL
);
114 return o
!= null ? (bool) o
: false;
116 set { AppDomain.CurrentDomain.SetData (Forms_requireSSL, value); }
118 static bool slidingExpiration
121 object o
= AppDomain
.CurrentDomain
.GetData (Forms_slidingExpiration
);
122 return o
!= null ? (bool) o
: false;
124 set { AppDomain.CurrentDomain.SetData (Forms_slidingExpiration, value); }
127 static bool requireSSL
;
128 static bool slidingExpiration
;
133 const string Forms_cookie_domain
= "Forms.cookie_domain";
134 const string Forms_cookie_mode
= "Forms.cookie_mode";
135 const string Forms_cookies_supported
= "Forms.cookies_supported";
136 const string Forms_default_url
= "Forms.default_url";
137 const string Forms_enable_crossapp_redirects
= "Forms.enable_crossapp_redirects";
138 const string Forms_login_url
= "Forms.login_url";
139 static string cookie_domain
141 get { return (string) AppDomain.CurrentDomain.GetData (Forms_cookie_domain); }
142 set { AppDomain.CurrentDomain.SetData (Forms_cookie_domain, value); }
144 static HttpCookieMode cookie_mode
146 get { return (HttpCookieMode) AppDomain.CurrentDomain.GetData (Forms_cookie_mode); }
147 set { AppDomain.CurrentDomain.SetData (Forms_cookie_mode, value); }
149 static bool cookies_supported
152 object o
= AppDomain
.CurrentDomain
.GetData (Forms_cookies_supported
);
153 return o
!= null ? (bool) o
: false;
155 set { AppDomain.CurrentDomain.SetData (Forms_cookies_supported, value); }
157 static string default_url
159 get { return (string) AppDomain.CurrentDomain.GetData (Forms_default_url); }
160 set { AppDomain.CurrentDomain.SetData (Forms_default_url, value); }
162 static bool enable_crossapp_redirects
165 object o
= AppDomain
.CurrentDomain
.GetData (Forms_enable_crossapp_redirects
);
166 return o
!= null ? (bool) o
: false;
168 set { AppDomain.CurrentDomain.SetData (Forms_enable_crossapp_redirects, value); }
170 static string login_url
172 get { return (string) AppDomain.CurrentDomain.GetData (Forms_login_url); }
173 set { AppDomain.CurrentDomain.SetData (Forms_login_url, value); }
176 static string cookie_domain
;
177 static HttpCookieMode cookie_mode
;
178 static bool cookies_supported
;
179 static string default_url
;
180 static bool enable_crossapp_redirects
;
181 static string login_url
;
184 // same names and order used in xsp
185 static string [] indexFiles
= { "index.aspx",
191 public FormsAuthentication ()
195 public static bool Authenticate (string name
, string password
)
197 if (name
== null || password
== null)
201 HttpContext context
= HttpContext
.Current
;
203 throw new HttpException ("Context is null!");
206 AuthenticationSection section
= (AuthenticationSection
) WebConfigurationManager
.GetSection (authConfigPath
);
207 FormsAuthenticationCredentials config
= section
.Forms
.Credentials
;
208 FormsAuthenticationUser user
= config
.Users
[name
];
209 string stored
= null;
212 stored
= user
.Password
;
214 AuthConfig config
= context
.GetConfig (authConfigPath
) as AuthConfig
;
215 Hashtable users
= config
.CredentialUsers
;
216 string stored
= users
[name
] as string;
221 switch (config
.PasswordFormat
) {
222 case FormsAuthPasswordFormat
.Clear
:
225 case FormsAuthPasswordFormat
.MD5
:
226 password
= HashPasswordForStoringInConfigFile (password
, "MD5");
228 case FormsAuthPasswordFormat
.SHA1
:
229 password
= HashPasswordForStoringInConfigFile (password
, "SHA1");
233 return (password
== stored
);
236 static FormsAuthenticationTicket
Decrypt2 (byte [] bytes
)
238 if (protection
== FormsProtectionEnum
.None
)
239 return FormsAuthenticationTicket
.FromByteArray (bytes
);
242 MachineKeySection config
= (MachineKeySection
) WebConfigurationManager
.GetSection (machineKeyConfigPath
);
244 MachineKeyConfig config
= HttpContext
.GetAppConfig (machineKeyConfigPath
) as MachineKeyConfig
;
246 bool all
= (protection
== FormsProtectionEnum
.All
);
248 byte [] result
= bytes
;
249 if (all
|| protection
== FormsProtectionEnum
.Encryption
) {
250 ICryptoTransform decryptor
;
251 decryptor
= TripleDES
.Create ().CreateDecryptor (config
.DecryptionKey192Bits
, init_vector
);
252 result
= decryptor
.TransformFinalBlock (bytes
, 0, bytes
.Length
);
256 if (all
|| protection
== FormsProtectionEnum
.Validation
) {
258 MachineKeyValidation validationType
;
261 validationType
= config
.Validation
;
263 validationType
= config
.ValidationType
;
265 if (validationType
== MachineKeyValidation
.MD5
)
266 count
= MD5_hash_size
;
268 count
= SHA1_hash_size
; // 3DES and SHA1
271 byte [] vk
= config
.ValidationKeyBytes
;
273 byte [] vk
= config
.ValidationKey
;
275 byte [] mix
= new byte [result
.Length
- count
+ vk
.Length
];
276 Buffer
.BlockCopy (result
, 0, mix
, 0, result
.Length
- count
);
277 Buffer
.BlockCopy (vk
, 0, mix
, result
.Length
- count
, vk
.Length
);
280 switch (validationType
) {
281 case MachineKeyValidation
.MD5
:
282 hash
= MD5
.Create ().ComputeHash (mix
);
284 // From MS docs: "When 3DES is specified, forms authentication defaults to SHA1"
285 case MachineKeyValidation
.TripleDES
:
286 case MachineKeyValidation
.SHA1
:
287 hash
= SHA1
.Create ().ComputeHash (mix
);
291 if (result
.Length
< count
)
292 throw new ArgumentException ("Error validating ticket (length).", "encryptedTicket");
295 for (i
= result
.Length
- count
, k
= 0; k
< count
; i
++, k
++) {
296 if (result
[i
] != hash
[k
])
297 throw new ArgumentException ("Error validating ticket.", "encryptedTicket");
301 return FormsAuthenticationTicket
.FromByteArray (result
);
304 public static FormsAuthenticationTicket
Decrypt (string encryptedTicket
)
306 if (encryptedTicket
== null || encryptedTicket
== String
.Empty
)
307 throw new ArgumentException ("Invalid encrypted ticket", "encryptedTicket");
311 FormsAuthenticationTicket ticket
;
313 byte [] bytes
= MachineKeySection
.GetBytes (encryptedTicket
, encryptedTicket
.Length
);
315 byte [] bytes
= MachineKeyConfig
.GetBytes (encryptedTicket
, encryptedTicket
.Length
);
318 ticket
= Decrypt2 (bytes
);
319 } catch (Exception
) {
326 public static string Encrypt (FormsAuthenticationTicket ticket
)
329 throw new ArgumentNullException ("ticket");
332 byte [] ticket_bytes
= ticket
.ToByteArray ();
333 if (protection
== FormsProtectionEnum
.None
)
334 return GetHexString (ticket_bytes
);
336 byte [] result
= ticket_bytes
;
338 MachineKeySection config
= (MachineKeySection
) WebConfigurationManager
.GetSection (machineKeyConfigPath
);
340 MachineKeyConfig config
= HttpContext
.GetAppConfig (machineKeyConfigPath
) as MachineKeyConfig
;
342 bool all
= (protection
== FormsProtectionEnum
.All
);
343 if (all
|| protection
== FormsProtectionEnum
.Validation
) {
344 byte [] valid_bytes
= null;
346 byte [] vk
= config
.ValidationKeyBytes
;
348 byte [] vk
= config
.ValidationKey
;
350 byte [] mix
= new byte [ticket_bytes
.Length
+ vk
.Length
];
351 Buffer
.BlockCopy (ticket_bytes
, 0, mix
, 0, ticket_bytes
.Length
);
352 Buffer
.BlockCopy (vk
, 0, mix
, result
.Length
, vk
.Length
);
358 config
.ValidationType
361 case MachineKeyValidation
.MD5
:
362 valid_bytes
= MD5
.Create ().ComputeHash (mix
);
364 // From MS docs: "When 3DES is specified, forms authentication defaults to SHA1"
365 case MachineKeyValidation
.TripleDES
:
366 case MachineKeyValidation
.SHA1
:
367 valid_bytes
= SHA1
.Create ().ComputeHash (mix
);
371 int tlen
= ticket_bytes
.Length
;
372 int vlen
= valid_bytes
.Length
;
373 result
= new byte [tlen
+ vlen
];
374 Buffer
.BlockCopy (ticket_bytes
, 0, result
, 0, tlen
);
375 Buffer
.BlockCopy (valid_bytes
, 0, result
, tlen
, vlen
);
378 if (all
|| protection
== FormsProtectionEnum
.Encryption
) {
379 ICryptoTransform encryptor
;
380 encryptor
= TripleDES
.Create ().CreateEncryptor (config
.DecryptionKey192Bits
, init_vector
);
381 result
= encryptor
.TransformFinalBlock (result
, 0, result
.Length
);
384 return GetHexString (result
);
387 public static HttpCookie
GetAuthCookie (string userName
, bool createPersistentCookie
)
389 return GetAuthCookie (userName
, createPersistentCookie
, null);
392 public static HttpCookie
GetAuthCookie (string userName
, bool createPersistentCookie
, string strCookiePath
)
396 if (userName
== null)
397 userName
= String
.Empty
;
399 if (strCookiePath
== null || strCookiePath
.Length
== 0)
400 strCookiePath
= cookiePath
;
402 DateTime now
= DateTime
.Now
;
404 if (createPersistentCookie
)
405 then
= now
.AddYears (50);
407 then
= now
.AddMinutes (timeout
);
409 FormsAuthenticationTicket ticket
= new FormsAuthenticationTicket (1,
413 createPersistentCookie
,
417 if (!createPersistentCookie
)
418 then
= DateTime
.MinValue
;
420 HttpCookie cookie
= new HttpCookie (cookieName
, Encrypt (ticket
), strCookiePath
, then
);
422 cookie
.Secure
= true;
426 internal static string ReturnUrl
428 get { return HttpContext.Current.Request ["RETURNURL"]; }
431 public static string GetRedirectUrl (string userName
, bool createPersistentCookie
)
433 if (userName
== null)
437 HttpRequest request
= HttpContext
.Current
.Request
;
438 string returnUrl
= ReturnUrl
;
439 if (returnUrl
!= null)
442 returnUrl
= request
.ApplicationPath
;
443 string apppath
= request
.PhysicalApplicationPath
;
446 foreach (string indexFile
in indexFiles
) {
447 string filePath
= Path
.Combine (apppath
, indexFile
);
448 if (File
.Exists (filePath
)) {
449 returnUrl
= UrlUtils
.Combine (returnUrl
, indexFile
);
456 returnUrl
= UrlUtils
.Combine (returnUrl
, "index.aspx");
461 static string GetHexString (byte [] bytes
)
463 StringBuilder result
= new StringBuilder (bytes
.Length
* 2);
464 foreach (byte b
in bytes
)
465 result
.AppendFormat ("{0:X2}", (int) b
);
467 return result
.ToString ();
470 public static string HashPasswordForStoringInConfigFile (string password
, string passwordFormat
)
472 if (password
== null)
473 throw new ArgumentNullException ("password");
475 if (passwordFormat
== null)
476 throw new ArgumentNullException ("passwordFormat");
479 if (String
.Compare (passwordFormat
, "MD5", true) == 0) {
480 bytes
= MD5
.Create ().ComputeHash (Encoding
.UTF8
.GetBytes (password
));
481 } else if (String
.Compare (passwordFormat
, "SHA1", true) == 0) {
482 bytes
= SHA1
.Create ().ComputeHash (Encoding
.UTF8
.GetBytes (password
));
484 throw new ArgumentException ("The format must be either MD5 or SHA1", "passwordFormat");
487 return GetHexString (bytes
);
490 public static void Initialize ()
500 AuthenticationSection section
= (AuthenticationSection
)WebConfigurationManager
.GetSection (authConfigPath
);
501 FormsAuthenticationConfiguration config
= section
.Forms
;
503 cookieName
= config
.Name
;
504 timeout
= (int)config
.Timeout
.TotalMinutes
;
505 cookiePath
= config
.Path
;
506 protection
= config
.Protection
;
507 requireSSL
= config
.RequireSSL
;
508 slidingExpiration
= config
.SlidingExpiration
;
509 cookie_domain
= config
.Domain
;
510 cookie_mode
= config
.Cookieless
;
511 cookies_supported
= true; /* XXX ? */
512 default_url
= MapUrl(config
.DefaultUrl
);
513 enable_crossapp_redirects
= config
.EnableCrossAppRedirects
;
514 login_url
= MapUrl(config
.LoginUrl
);
516 HttpContext context
= HttpContext
.Current
;
517 AuthConfig authConfig
= context
.GetConfig (authConfigPath
) as AuthConfig
;
518 if (authConfig
!= null) {
519 cookieName
= authConfig
.CookieName
;
520 timeout
= authConfig
.Timeout
;
521 cookiePath
= authConfig
.CookiePath
;
522 protection
= authConfig
.Protection
;
524 requireSSL
= authConfig
.RequireSSL
;
525 slidingExpiration
= authConfig
.SlidingExpiration
;
528 cookieName
= ".MONOAUTH";
531 protection
= FormsProtectionEnum
.All
;
533 slidingExpiration
= true;
538 // IV is 8 bytes long for 3DES
539 init_vector
= new byte [8];
540 int len
= cookieName
.Length
;
541 for (int i
= 0; i
< 8; i
++) {
545 init_vector
[i
] = (byte) cookieName
[i
];
553 static string MapUrl (string url
) {
554 if (UrlUtils
.IsRelativeUrl (url
))
555 return UrlUtils
.Combine (HttpRuntime
.AppDomainAppVirtualPath
, url
);
557 return UrlUtils
.ResolveVirtualPathFromAppAbsolute (url
);
561 public static void RedirectFromLoginPage (string userName
, bool createPersistentCookie
)
563 RedirectFromLoginPage (userName
, createPersistentCookie
, null);
566 public static void RedirectFromLoginPage (string userName
, bool createPersistentCookie
, string strCookiePath
)
568 if (userName
== null)
572 SetAuthCookie (userName
, createPersistentCookie
, strCookiePath
);
573 Redirect (GetRedirectUrl (userName
, createPersistentCookie
), false);
576 public static FormsAuthenticationTicket
RenewTicketIfOld (FormsAuthenticationTicket tOld
)
581 DateTime now
= DateTime
.Now
;
582 TimeSpan toIssue
= now
- tOld
.IssueDate
;
583 TimeSpan toExpiration
= tOld
.Expiration
- now
;
584 if (toExpiration
> toIssue
)
587 FormsAuthenticationTicket tNew
= tOld
.Clone ();
588 tNew
.SetDates (now
, now
+ (tOld
.Expiration
- tOld
.IssueDate
));
592 public static void SetAuthCookie (string userName
, bool createPersistentCookie
)
595 SetAuthCookie (userName
, createPersistentCookie
, cookiePath
);
598 public static void SetAuthCookie (string userName
, bool createPersistentCookie
, string strCookiePath
)
600 HttpContext context
= HttpContext
.Current
;
602 throw new HttpException ("Context is null!");
604 HttpResponse response
= context
.Response
;
605 if (response
== null)
606 throw new HttpException ("Response is null!");
608 response
.Cookies
.Add (GetAuthCookie (userName
, createPersistentCookie
, strCookiePath
));
611 public static void SignOut ()
615 HttpContext context
= HttpContext
.Current
;
617 throw new HttpException ("Context is null!");
619 HttpResponse response
= context
.Response
;
620 if (response
== null)
621 throw new HttpException ("Response is null!");
623 HttpCookieCollection cc
= response
.Cookies
;
624 cc
.Remove (cookieName
);
625 HttpCookie expiration_cookie
= new HttpCookie (cookieName
, "");
626 expiration_cookie
.Expires
= new DateTime (1999, 10, 12);
627 expiration_cookie
.Path
= cookiePath
;
628 cc
.Add (expiration_cookie
);
631 public static string FormsCookieName
639 public static string FormsCookiePath
647 public static bool RequireSSL
{
654 public static bool SlidingExpiration
{
657 return slidingExpiration
;
663 public static string CookieDomain
{
664 get { Initialize (); return cookie_domain; }
667 public static HttpCookieMode CookieMode
{
668 get { Initialize (); return cookie_mode; }
671 public static bool CookiesSupported
{
672 get { Initialize (); return cookies_supported; }
675 public static string DefaultUrl
{
676 get { Initialize (); return default_url; }
679 public static bool EnableCrossAppRedirects
{
680 get { Initialize (); return enable_crossapp_redirects; }
683 public static string LoginUrl
{
684 get { Initialize (); return login_url; }
687 public static void RedirectToLoginPage ()
692 [MonoTODO ("needs more tests")]
693 public static void RedirectToLoginPage (string extraQueryString
)
695 // TODO: if ? is in LoginUrl (legal?), ? in query (legal?) ...
696 Redirect (LoginUrl
+ "?" + extraQueryString
);
699 private static void Redirect (string url
)
701 HttpContext
.Current
.Response
.Redirect (url
);
705 private static void Redirect (string url
, bool end
)
707 HttpContext
.Current
.Response
.Redirect (url
, end
);