1 //------------------------------------------------------------------------------
2 // <copyright file="AnonymousIdentificationModule.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6 
7 /*
8  * AnonymousIdentificationModule class
9  *
10  * Copyright (c) 1999 Microsoft Corporation
11  */
12 
13 namespace System.Web.Security {
14     using System.Web;
15     using System.Text;
16     using System.Web.Configuration;
17     using System.Web.Caching;
18     using System.Web.Handlers;
19     using System.Collections;
20     using System.Configuration.Provider;
21     using System.Web.Util;
22     using System.Security.Principal;
23     using System.Security.Permissions;
24     using System.Globalization;
25     using System.Web.Management;
26     using System.Web.Hosting;
27     using System.IO;
28     using System.Runtime.Serialization.Formatters.Binary;
29     using System.Web.Security.Cryptography;
30 
31     /// <devdoc>
32     ///    <para>[To be supplied.]</para>
33     /// </devdoc>
34     public sealed class AnonymousIdentificationModule : IHttpModule {
35 
36         private const int MAX_ENCODED_COOKIE_STRING = 512;
37         private const int MAX_ID_LENGTH             = 128;
38 
39         /// <devdoc>
40         ///    <para>
41         ///       Initializes a new instance of the <see cref='System.Web.Security.AnonymousIdentificationModule'/>
42         ///       class.
43         ///     </para>
44         /// </devdoc>
45         [SecurityPermission(SecurityAction.Demand, Unrestricted=true)]
AnonymousIdentificationModule()46         public AnonymousIdentificationModule() {
47         }
48 
49         /////////////////////////////////////////////////////////////////////////////
50         /////////////////////////////////////////////////////////////////////////////
51         /////////////////////////////////////////////////////////////////////////////
52         // Events
53         private AnonymousIdentificationEventHandler _CreateNewIdEventHandler;
54 
55         public event AnonymousIdentificationEventHandler Creating {
56             add    {  _CreateNewIdEventHandler += value; }
57             remove {  _CreateNewIdEventHandler -= value; }
58         }
59 
ClearAnonymousIdentifier()60         public static void ClearAnonymousIdentifier()
61         {
62             if (!s_Initialized)
63                 Initialize();
64 
65             HttpContext context = HttpContext.Current;
66             if (context == null)
67                 return;
68 
69             // VSWhidbey 418835: When this feature is enabled, prevent infinite loop when cookieless
70             // mode != Cookies and there was no cookie, also we cannot clear when current user is anonymous.
71             if (!s_Enabled || !context.Request.IsAuthenticated) {
72                 throw new NotSupportedException(SR.GetString(SR.Anonymous_ClearAnonymousIdentifierNotSupported));
73             }
74 
75             ////////////////////////////////////////////////////////////
76             // Check if we need to clear the ticket stored in the URI
77             bool clearUri = false;
78             if (context.CookielessHelper.GetCookieValue('A') != null) {
79                 context.CookielessHelper.SetCookieValue('A', null); // Always clear the uri-cookie
80                 clearUri = true;
81             }
82 
83             ////////////////////////////////////////////////////////////
84             // Clear cookie if cookies are supported by the browser
85             if (!CookielessHelperClass.UseCookieless(context, false, s_CookieMode) || context.Request.Browser.Cookies)
86             { // clear cookie if required
87                 string cookieValue = String.Empty;
88                 if (context.Request.Browser["supportsEmptyStringInCookieValue"] == "false")
89                     cookieValue = "NoCookie";
90                 HttpCookie cookie = new HttpCookie(s_CookieName, cookieValue);
91                 cookie.HttpOnly = true;
92                 cookie.Path = s_CookiePath;
93                 cookie.Secure = s_RequireSSL;
94                 if (s_Domain != null)
95                     cookie.Domain = s_Domain;
96                 cookie.Expires = new System.DateTime(1999, 10, 12);
97                 context.Response.Cookies.RemoveCookie(s_CookieName);
98                 context.Response.Cookies.Add(cookie);
99             }
100 
101             ////////////////////////////////////////////////////////////
102             // Redirect back to this page if we removed a URI ticket
103             if (clearUri) {
104                 context.Response.Redirect(context.Request.RawUrl, false);
105             }
106         }
107         /////////////////////////////////////////////////////////////////////////////
108         /////////////////////////////////////////////////////////////////////////////
109         /////////////////////////////////////////////////////////////////////////////
110         // Public functions
111 
Dispose()112         public void Dispose() {  }
113 
Init(HttpApplication app)114         public void Init(HttpApplication app) {
115             // for IIS 7, skip event wireup altogether if this feature isn't
116             // enabled
117             if (!s_Initialized) {
118                 Initialize();
119             }
120             if (s_Enabled) {
121                 app.PostAuthenticateRequest += new EventHandler(this.OnEnter);
122             }
123         }
124 
125         ////////////////////////////////////////////////////////////
126         ////////////////////////////////////////////////////////////
127         ////////////////////////////////////////////////////////////
128 
129         /// <devdoc>
130         ///    <para>[To be supplied.]</para>
131         /// </devdoc>
OnEnter(Object source, EventArgs eventArgs)132         private void OnEnter(Object source, EventArgs eventArgs) {
133             if (!s_Initialized)
134                 Initialize();
135 
136             if (!s_Enabled)
137                 return;
138 
139             HttpApplication  app;
140             HttpContext      context;
141             HttpCookie       cookie = null;
142             bool             createCookie = false;
143             AnonymousIdData  decodedValue = null;
144             bool             cookieLess;
145             string           encValue = null;
146             bool             isAuthenticated = false;
147 
148             app = (HttpApplication)source;
149             context = app.Context;
150 
151             isAuthenticated = context.Request.IsAuthenticated;
152 
153             if (isAuthenticated) {
154                 cookieLess = CookielessHelperClass.UseCookieless(context, false /* no redirect */, s_CookieMode);
155             } else {
156                 cookieLess = CookielessHelperClass.UseCookieless(context, true /* do redirect */, s_CookieMode);
157                 //if (!cookieLess && s_RequireSSL && !context.Request.IsSecureConnection)
158                 //    throw new HttpException(SR.GetString(SR.Connection_not_secure_creating_secure_cookie));
159             }
160 
161             ////////////////////////////////////////////////////////////////////////
162             // Handle secure-cookies over non SSL
163             if (s_RequireSSL && !context.Request.IsSecureConnection)
164             {
165                 if (!cookieLess)
166                 {
167                     cookie = context.Request.Cookies[s_CookieName];
168                     if (cookie != null)
169                     {
170                         cookie = new HttpCookie(s_CookieName, String.Empty);
171                         cookie.HttpOnly = true;
172                         cookie.Path = s_CookiePath;
173                         cookie.Secure = s_RequireSSL;
174                         if (s_Domain != null)
175                             cookie.Domain = s_Domain;
176                         cookie.Expires = new System.DateTime(1999, 10, 12);
177 
178                         if (context.Request.Browser["supportsEmptyStringInCookieValue"] == "false")
179                             cookie.Value = "NoCookie";
180                         context.Response.Cookies.Add(cookie);
181                     }
182 
183                     return;
184                 }
185             }
186 
187 
188             ////////////////////////////////////////////////////////////
189             // Step 2: See if cookie, or cookie-header has the value
190             if (!cookieLess)
191             {
192                 cookie = context.Request.Cookies[s_CookieName];
193                 if (cookie != null)
194                 {
195                     encValue = cookie.Value;
196                     cookie.Path = s_CookiePath;
197                     if (s_Domain != null)
198                         cookie.Domain = s_Domain;
199                 }
200             }
201             else
202             {
203                 encValue = context.CookielessHelper.GetCookieValue('A');
204             }
205 
206             decodedValue = GetDecodedValue(encValue);
207 
208             if (decodedValue != null && decodedValue.AnonymousId != null) {
209                 // Copy existing value in Request
210                 context.Request.AnonymousID = decodedValue.AnonymousId;
211             }
212             if (isAuthenticated) // For the authenticated case, we are done
213                 return;
214 
215             if (context.Request.AnonymousID == null) {
216                 ////////////////////////////////////////////////////////////
217                 // Step 3: Create new Identity
218 
219                 // Raise event
220                 if (_CreateNewIdEventHandler != null) {
221                     AnonymousIdentificationEventArgs e = new AnonymousIdentificationEventArgs(context);
222                     _CreateNewIdEventHandler(this, e);
223                     context.Request.AnonymousID = e.AnonymousID;
224                 }
225 
226                 // Create from GUID
227                 if (string.IsNullOrEmpty(context.Request.AnonymousID)) {
228                     context.Request.AnonymousID = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);
229                 } else {
230                     if (context.Request.AnonymousID.Length > MAX_ID_LENGTH)
231                         throw new HttpException(SR.GetString(SR.Anonymous_id_too_long));
232                 }
233                 if (s_RequireSSL && !context.Request.IsSecureConnection && !cookieLess)
234                     return; // Don't create secure-cookie in un-secured connection
235 
236                 createCookie = true;
237             }
238 
239             ////////////////////////////////////////////////////////////
240             // Step 4: Check if cookie has to be created
241             DateTime dtNow = DateTime.UtcNow;
242             if (!createCookie) {
243                 if (s_SlidingExpiration) {
244                     if (decodedValue == null || decodedValue.ExpireDate < dtNow) {
245                         createCookie = true;
246                     } else {
247                         double secondsLeft =  (decodedValue.ExpireDate - dtNow).TotalSeconds;
248                         if (secondsLeft < (double) ((s_CookieTimeout*60)/2)) {
249                             createCookie = true;
250                         }
251                     }
252                 }
253             }
254 
255             ////////////////////////////////////////////////////////////
256             // Step 4: Create new cookie or cookieless header
257             if (createCookie) {
258                 DateTime dtExpireTime = dtNow.AddMinutes(s_CookieTimeout);
259                 encValue = GetEncodedValue(new AnonymousIdData(context.Request.AnonymousID, dtExpireTime));
260                 if (encValue.Length > MAX_ENCODED_COOKIE_STRING)
261                     throw new HttpException(SR.GetString(SR.Anonymous_id_too_long_2));
262 
263                 if (!cookieLess) {
264                     cookie          = new HttpCookie(s_CookieName, encValue);
265                     cookie.HttpOnly = true;
266                     cookie.Expires  = dtExpireTime;
267                     cookie.Path     = s_CookiePath;
268                     cookie.Secure   = s_RequireSSL;
269                     if (s_Domain != null)
270                         cookie.Domain   = s_Domain;
271                     context.Response.Cookies.Add(cookie);
272                 } else {
273                     context.CookielessHelper.SetCookieValue('A', encValue);
274                     context.Response.Redirect(context.Request.RawUrl);
275                 }
276             }
277         }
278 
279         public static bool Enabled {
280             get {
281                 if (!s_Initialized) {
282                     Initialize();
283                 }
284                 return s_Enabled;
285             }
286         }
287 
288         /////////////////////////////////////////////////////////////////////////////
289         /////////////////////////////////////////////////////////////////////////////
290         /////////////////////////////////////////////////////////////////////////////
291         // Static config settings
292         private static bool       s_Initialized         = false;
293         private static bool       s_Enabled             = false;
294         private static string     s_CookieName          = ".ASPXANONYMOUS";
295         private static string     s_CookiePath          = "/";
296         private static int        s_CookieTimeout       = 100000;
297         private static bool       s_RequireSSL          = false;
298         private static string     s_Domain              = null;
299         private static bool       s_SlidingExpiration   = true;
300         private static byte []    s_Modifier            = null;
301         private static object     s_InitLock            = new object();
302         private static HttpCookieMode   s_CookieMode    = HttpCookieMode.UseDeviceProfile;
303         private static CookieProtection s_Protection    = CookieProtection.None;
304 
305         /////////////////////////////////////////////////////////////////////////////
306         /////////////////////////////////////////////////////////////////////////////
307         /////////////////////////////////////////////////////////////////////////////
308         // Static functions
Initialize()309         private static void Initialize() {
310 
311             if (s_Initialized)
312                 return;
313 
314             lock(s_InitLock) {
315                 if (s_Initialized)
316                     return;
317 
318                 AnonymousIdentificationSection settings = RuntimeConfig.GetAppConfig().AnonymousIdentification;
319                 s_Enabled            = settings.Enabled;
320                 s_CookieName         = settings.CookieName;
321                 s_CookiePath         = settings.CookiePath;
322                 s_CookieTimeout      = (int) settings.CookieTimeout.TotalMinutes;
323                 s_RequireSSL         = settings.CookieRequireSSL;
324                 s_SlidingExpiration  = settings.CookieSlidingExpiration;
325                 s_Protection         = settings.CookieProtection;
326                 s_CookieMode         = settings.Cookieless;
327                 s_Domain             = settings.Domain;
328 
329                 s_Modifier = Encoding.UTF8.GetBytes("AnonymousIdentification");
330                 if (s_CookieTimeout < 1)
331                     s_CookieTimeout = 1;
332                 if (s_CookieTimeout > 60 * 24 * 365 * 2)
333                     s_CookieTimeout = 60 * 24 * 365 * 2; // 2 years
334                 s_Initialized = true;
335             }
336         }
337 
338         /////////////////////////////////////////////////////////////////////////////
339         /////////////////////////////////////////////////////////////////////////////
GetEncodedValue(AnonymousIdData data)340         private static string GetEncodedValue(AnonymousIdData data){
341             if (data == null)
342                 return null;
343             byte [] bufId       = Encoding.UTF8.GetBytes(data.AnonymousId);
344             byte [] bufIdLen    = BitConverter.GetBytes(bufId.Length);
345             byte [] bufDate     = BitConverter.GetBytes(data.ExpireDate.ToFileTimeUtc());
346             byte [] buffer      = new byte[12 + bufId.Length];
347 
348             Buffer.BlockCopy(bufDate,   0, buffer, 0,  8);
349             Buffer.BlockCopy(bufIdLen,  0, buffer, 8,  4);
350             Buffer.BlockCopy(bufId,     0, buffer, 12, bufId.Length);
351             return CookieProtectionHelper.Encode(s_Protection, buffer, Purpose.AnonymousIdentificationModule_Ticket);
352         }
353 
354         /////////////////////////////////////////////////////////////////////////////
355         /////////////////////////////////////////////////////////////////////////////
GetDecodedValue(string data)356         private static AnonymousIdData GetDecodedValue(string data){
357             if (data == null || data.Length < 1 || data.Length > MAX_ENCODED_COOKIE_STRING)
358                 return null;
359 
360             try {
361                 byte [] bBlob = CookieProtectionHelper.Decode(s_Protection, data, Purpose.AnonymousIdentificationModule_Ticket);
362                 if (bBlob == null || bBlob.Length < 13)
363                     return null;
364                 DateTime expireDate = DateTime.FromFileTimeUtc(BitConverter.ToInt64(bBlob, 0));
365                 if (expireDate < DateTime.UtcNow)
366                     return null;
367                 int len = BitConverter.ToInt32(bBlob, 8);
368                 if (len < 0 || len > bBlob.Length - 12)
369                     return null;
370                 string id = Encoding.UTF8.GetString(bBlob, 12, len);
371                 if (id.Length > MAX_ID_LENGTH)
372                     return null;
373                 return new AnonymousIdData(id, expireDate);
374             }
375             catch {}
376             return null;
377         }
378 
379         /////////////////////////////////////////////////////////////////////////////
380         /////////////////////////////////////////////////////////////////////////////
381     }
382 
383     /////////////////////////////////////////////////////////////////////////////
384     /////////////////////////////////////////////////////////////////////////////
385     /////////////////////////////////////////////////////////////////////////////
386     [Serializable]
387     internal class AnonymousIdData
388     {
AnonymousIdData(string id, DateTime dt)389         internal AnonymousIdData(string id, DateTime dt) {
390             ExpireDate = dt;
391             AnonymousId = (dt > DateTime.UtcNow) ? id : null;  // Ignore expired data
392         }
393 
394         internal string     AnonymousId;
395         internal DateTime   ExpireDate;
396     }
397 
398     /////////////////////////////////////////////////////////////////////////////
399     /////////////////////////////////////////////////////////////////////////////
400     /////////////////////////////////////////////////////////////////////////////
401 
AnonymousIdentificationEventHandler(object sender, AnonymousIdentificationEventArgs e)402     public delegate void AnonymousIdentificationEventHandler(object sender, AnonymousIdentificationEventArgs e);
403 
404     /////////////////////////////////////////////////////////////////////////////
405     /////////////////////////////////////////////////////////////////////////////
406     /////////////////////////////////////////////////////////////////////////////
407 
408     public sealed class AnonymousIdentificationEventArgs : EventArgs {
409 
410         public string         AnonymousID { get { return _AnonymousId;} set { _AnonymousId = value;}}
411 
412         public HttpContext    Context     { get { return _Context; }}
413 
414         private string        _AnonymousId;
415         private HttpContext   _Context;
416 
417 
AnonymousIdentificationEventArgs(HttpContext context)418         public AnonymousIdentificationEventArgs(HttpContext context) {
419             _Context = context;
420         }
421     }
422 
423 }
424 
425 
426