CHAPTER 6
The source code presented in this section is in the folder Examples\Chapter 6 in the Bitbucket repository. The Visual Studio solution file is in the Chapter 6\Clifton.WebServer folder.
Handlers are stateless—they have to be because the same code could be executing on hundreds of threads. However, there obviously is a need to maintain information between requests, typically an authorization token, a user name, the last request time, and so forth. These pieces of information are all managed in a stateful session that is associated with the user’s IP address.
The session management provided here is really a basic implementation, and I certainly don’t want to presume what your authorization, session expiration, and user role management needs might be. As we saw in the chapter covering routing, you can use the routing handler that is “in the can,” or you can provide a different routing handler to the workflow.
In this chapter we’ll add two separate workflow steps: one for checking session expiration, and the other for checking authorization. As mentioned in the previous chapter, these are entwined with the request verb and path. So, like the route provider, we’ll be implementing a test to see if a provider exists, and if so, continue or terminate the workflow based on the provider’s response.
We should also talk about cross-site request forgery (CSRF) when we’re discussing sessions, as this is a token that is preserved within the context of a session.
First, we need a container for the concept of a session. In the following implementation, note that the session provides three things:
/// <summary> /// Sessions are associated with the client IP. /// </summary> public class Session { public DateTime LastConnection { get; set; } /// <summary> /// Is the user authorized? /// </summary> public bool Authorized { get; set; } /// <summary> /// This flag is set by the session manager if the session has expired between /// the last connection timestamp and the current connection timestamp. /// </summary> public bool Expired { get; set; } /// <summary> /// Can be used by controllers to add additional information that needs /// </summary> private ConcurrentDictionary<string, object> Objects { get; set; } // Indexer for accessing session objects. If an object isn't found, public object this[string objectKey] { get { object val = null; Objects.TryGetValue(objectKey, out val); return val; } set { Objects[objectKey] = value; } } /// <summary> /// Object collection getter with type conversion. /// Note that if the object does not exist in the session, the default /// Therefore, session objects like "isAdmin" or "isAuthenticated" /// </summary> public T GetObject<T>(string objectKey) { object val = null; T ret = default(T); if (Objects.TryGetValue(objectKey, out val)) { ret = (T)Converter.Convert(val, typeof(T)); } return ret; } public Session() { Objects = new ConcurrentDictionary<string, object>(); UpdateLastConnectionTime(); } public void UpdateLastConnectionTime() { LastConnection = DateTime.Now; } /// <summary> /// Returns true if the last request exceeds the specified expiration /// </summary> public bool IsExpired(int expirationInSeconds) { return (DateTime.Now - LastConnection).TotalSeconds > expirationInSeconds; } /// <summary> /// De-authorize the session. /// </summary> public void Expire() { Authenticated = false; Expired = true; } } |
Code Listing 48
Note also that we’re using a ConcurrentDictionary, as it is possible that the application may, unbeknownst to us, set up its own worker threads in a request, where each thread might be concurrently accessing session information.
Next, we need a session manager. The session manager creates the session if it doesn’t exist. If it does exist, it updates the Expired flag if the session has expired—this is based on whether the time since the last request exceeds the expiration time, which is set to 10 minutes by default.
public class SessionManager { public string CsrfTokenName { get; set; } public int ExpireInSeconds { get; set; } /// <summary> /// Track all sessions. /// </summary> protected ConcurrentDictionary<IPAddress, Session> sessionMap; public SessionManager(RouteTable routeTable) { sessionMap = new ConcurrentDictionary<IPAddress, Session>(); CsrfTokenName = "_CSRF_"; ExpireInSeconds = 10 * 60; } public WorkflowState Provider( { Session session; IPAddress endpointAddress = context.EndpointAddress(); if (!sessionMap.TryGetValue(endpointAddress, out session)) { session = new Session(); session[CsrfTokenName] = Guid.NewGuid().ToString(); sessionMap[endpointAddress] = session; } else { // If the session exists, set the expired flag before we // Once set, stays set until explicitly cleared. session.Expired |= session.IsExpired(ExpireInSeconds); } session.UpdateLastConnectionTime(); return ret; } protected WorkflowState CheckExpirationAndAuthorization( { WorkflowState ret = WorkflowState.Continue; RouteEntry entry = null; if (routeTable.TryGetRouteEntry(context.Verb(), context.Path(), out entry)) { if (entry.SessionExpirationProvider != null) { ret = entry.SessionExpirationProvider(workflowContinuation, context, session); } if (ret == WorkflowState.Continue) { if (entry.AuthorizationProvider != null) { ret = entry.AuthorizationProvider(workflowContinuation, context, session); } } } return WorkflowState.Continue; } } |
Code Listing 49
For new sessions, a CSRF token is registered. We can use this token when we render pages, embedding it into put, post, and delete requests to the server to protect the data on the server from someone maliciously forging user activity. We’ll discuss this in a later chapter on view engines.
Again, note the use of the ConcurrentDictionary, as we are most likely dealing with concurrent access to the server-wide session manager.
First, let’s create our sessionManager instance and add it to the workflow:
public static void InitializeSessionManager() { sessionManager = new SessionManager(routeTable); } |
Code Listing 50
Next we set up a couple of webpages that let us play with explicitly setting expiration and authorization. We want a page that tells us whether we have an expired or unauthorized request. As with the routing example, we’ll just throw an exception if the session is expired or unauthorized.
routeTable.AddRoute("get", "testsession", new RouteEntry() { SessionExpirationProvider = (continuation, context, session) => { if (session.Expired) { throw new ApplicationException("Session has expired!"); } else { return WorkflowState.Continue; } }, AuthorizationProvider = (continuation, context, session) => { if (!session.Authorized) { throw new ApplicationException("Not authorized!"); } else { return WorkflowState.Continue; } }, { context.RespondWith("<p>Looking good!</p>"); return WorkflowState.Done; } }); |
Code Listing 51
We also want a page that lets us set and clear the expired and authorized flags. Note that for the purposes of this demonstration, we do not test this page for expiration or authentication! Here we’ll have a little fun with URL parameters in the route handler:
routeTable.AddRoute("get", "SetState", new RouteEntry() { RoutingProvider = (continuation, context, session) => { Dictionary<string, string> parms = context.GetUrlParameters(); session.Expired = GetBooleanState(parms, "Expired", false); session.Authorized = GetBooleanState(parms, "Authorized", false); "<p>Expired has been set to " + session.Expired + "</p>"+ "<p>Authorized has been set to "+session.Authorized + "</p>"); return WorkflowState.Done; } }); |
Code Listing 52
We have a little helper function to convert some different ways of expressing yes and no to a boolean:
public static bool GetBooleanState( { bool ret = defaultValue; string val; if (parms.TryGetValue(key.ToLower(), out val)) { switch(val.ToLower()) { case "false": case "no": case "off": ret = false; break; case "true": case "yes": case "on": ret = true; break; } } return ret; } |
Code Listing 53
We’ll first test a non-expired, authorized site, which gives us back:

Figure 9: Not Expired, Authorized
Now when we test our state with the testsession URL, we get:

Figure 10: Looking Good!
We can expire the session:

Figure 11: Expire the Session
Which gives us:

Figure 12: Session Has Expired
Lastly, we can de-authorize the session:

Figure 13: De-authorize the Session
And we get:

Figure 14: The Session is No Longer Authorized
It’s important that we clean up expired sessions. To do that the question becomes: when do we really delete any knowledge of the session, versus potentially giving the user some feedback, such as “your session has expired, please log in again”? Truly deleting a session should happen sometime after it has expired, but ultimately, this is a decision for the developer creating the web application. At best, we can offer this function in the session manager that cleans up sessions with a specific “haven’t seen any user activity since this date/time” criteria:
public void CleanupDeadSessions(int deadAfterSeconds) { sessionMap.Values.Where(s => } |
Code Listing 54
It’s really up to you to decide when you want to call that function, but I suggest a worker thread that fires every minute or so.
In the previous code, I embed the session expiration and authorization checks as anonymous functions. This isn’t an easily re-usable pattern—I certainly do not recommend that you copy and paste a couple of anonymous methods for every route handler—it is simply to keep the code example tight.
You could, for example, add some extension methods to the RouteTable:
public static class RouteTableExtensions { /// <summary> /// Add a route with session expiration checking. /// </summary> public static void AddExpirableRoute(this RouteTable routeTable, string verb, string path, Func<WorkflowContinuation<HttpListenerContext>, HttpListenerContext, Session, { routeTable.AddRoute(verb, path, new RouteEntry() { SessionExpirationHandler = (continuation, context, session, parms) => { /* Your expiration check */ return WorkflowState.Continue; }, RouteHandler = routeHandler, }); } /// <summary> /// Add a route with session expiration and authorization checking. /// </summary> public static void AddExpirableAuthorizedRoute(this RouteTable routeTable, string verb, string path, Func<WorkflowContinuation<HttpListenerContext>, HttpListenerContext, Session, { routeTable.AddRoute(verb, path, new RouteEntry() { SessionExpirationHandler = (continuation, context, session, parms) => { /* Your expiration check */ return WorkflowState.Continue; }, AuthorizationHandler = (continuation, context, session, parms) => { /* Your authentication check */ return WorkflowState.Continue; }, RouteHandler = routeHandler, }); } } |
Code Listing 55
You can now re-use the expiration check and authorization check more easily; for example:
routeTable.AddExpirableRoute("get", "somepath", myRouteHandler); routeTable.AddExpirableAuthorizedRoute("get", "someotherpath", myRouteHandler); |
Code Listing 56
At this point, our web server is providing a lot of capability. We can:
However, there are still a few things left to do, such as:
We’ll look at these issues in the remaining chapters.