Sessions

HTTP is a stateless protocol. While some view this as a disadvantage, advocates of RESTful web development laud this as a plus. When state is removed from the picture, we get some automatic benefits, such as easier scalability and caching. You can draw many parallels with the non-mutable nature of Haskell in general.

As much as possible, RESTful applications should avoid storing state about an interaction with a client. However, it is sometimes unavoidable. Features like shopping carts are the classic example, but other more mundane interactions like proper login handling can be greatly enhanced by correct usage of sessions.

This chapter will describe how Yesod stores session data, how you can access this data, and some special functions to help you make the most of sessions.

Clientsession

One of the earliest packages spun off from Yesod was clientsession. This package uses encryption and signatures to store data in a client-side cookie. The encryption prevents the user from inspecting the data, and the signature ensures that the session cannot be tampered with.

It might sound like a bad idea from an efficiency standpoint to store data in a cookie. After all, this means that the data must be sent on every request. However, in practice, clientsession can be a great boon for performance.

  • No server side database lookup is required to service a request.

  • We can easily scale horizontally: each request contains all the information we need to send a response.

  • To avoid undue bandwidth overhead, production sites can serve their static content from a separate domain name, thereby skipping transmission of the session cookie for each request.

Storing megabytes of information in the session will be a bad idea. But for that matter, most session implementations recommend against such practices. If you really need massive storage for a user, it is best to store a lookup key in the session, and put the actual data in a database.

All of the interaction with clientsession is handled by Yesod internally, but there are a few spots where you can tweak the behavior just a bit.

Controlling sessions

By default, your Yesod application will use clientsession for its session storage, getting the encryption key from the client client-session-key.aes and giving a session a two hour timeout. (Note: timeout is measured from the last time the client sent a request to the site, not from when then session was first created.) However, all of those points can be modified by overriding the makeSessionBackend method in the Yesod typeclass.

One simple way to override this method is to simply turn off session handling; to do so, return Nothing. If your app has absolutely no session needs, disabling them can give a bit of a performance increase. But be careful about disabling sessions: this will also disable such features as Cross-Site Request Forgery protection.

  1. instance Yesod App where
  2. makeSessionBackend _ = return Nothing

Another common approach is to modify the filepath or timeout value, but continue using client-session. To do so, use the defaultClientSessionBackend helper function:

  1. instance Yesod App where
  2. makeSessionBackend _ =
  3. fmap Just $ defaultClientSessionBackend minutes filepath
  4. where minutes = 24 * 60 -- 1 day
  5. filepath = "mykey.aes"

There are a few other functions to grant you more fine-grained control of client-session, but they will rarely be necessary. Please see Yesod.Core‘s documentation if you are interested. It’s also possible to implement some other form of session, such as a server side session. To my knowledge, at the time of writing, no other such implementations exist.

If the given key file does not exist, it will be created and populated with a randomly generated key. When you deploy your app to production, you should include a pregenerated key with it, otherwise all existing sessions will be invalidated when your new key file is generated. The scaffolding addresses this for you.

Hardening via SSL

Client sessions over HTTP have an inherent hijacking vulnerability: an attacker can read the unencrypted traffic, obtain the session cookie from it, and then make requests to the site with that same cookie to impersonate the user. This vulnerability is particularly severe if the sessions include any personally identifiable information or authentication material.

The only sure way to defeat this threat is to run your entire site over SSL, and prevent browsers from attempting to access it over HTTP. You can achieve the first part of this at the webserver level, either via an SSL solution in Haskell such as warp-tls, or by using an SSL-enabled load balancer like Amazon Elastic Load Balancer.

To prevent your site from sending cookies over insecure connections, you should augment your application’s sessions as well as the default yesodMiddleware implementation with some additional behavior: Apply the sslOnlySessions transformation to your makeSessionBackend, and compose the sslOnlyMiddleware transformation with your yesodMiddleware implementation.

  1. instance Yesod App where
  2. makeSessionBackend _ = sslOnlySessions $
  3. fmap Just $ defaultClientSessionBackend 120 "mykey.aes"
  4. yesodMiddleware = (sslOnlyMiddleware 120) . defaultYesodMiddleware

sslOnlySessions causes all session cookies to be set with the Secure bit on, so that browsers will not transmit them over HTTP. sslOnlyMiddleware adds a Strict-Transport-Security header to all responses, which instructs browsers not to make HTTP requests to your domain or its subdomains for the specified number of minutes. Be sure to set the timeout for the sslOnlyMiddleware to be at least as long as your session timeout. Used together, these measures will ensure that session cookies are not transmitted in the clear.

Session Operations

Like most frameworks, a session in Yesod is a key-value store. The base session API boils down to four functions: lookupSession gets a value for a key (if available), getSession returns all of the key/value pairs, setSession sets a value for a key, and deleteSession clears a value for a key.

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE QuasiQuotes #-}
  3. {-# LANGUAGE TemplateHaskell #-}
  4. {-# LANGUAGE TypeFamilies #-}
  5. {-# LANGUAGE MultiParamTypeClasses #-}
  6. import Control.Applicative ((<$>), (<*>))
  7. import qualified Web.ClientSession as CS
  8. import Yesod
  9. data App = App
  10. mkYesod "App" [parseRoutes|
  11. / HomeR GET POST
  12. |]
  13. getHomeR :: Handler Html
  14. getHomeR = do
  15. sess <- getSession
  16. defaultLayout
  17. [whamlet|
  18. <form method=post>
  19. <input type=text name=key>
  20. <input type=text name=val>
  21. <input type=submit>
  22. <h1>#{show sess}
  23. |]
  24. postHomeR :: Handler ()
  25. postHomeR = do
  26. (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
  27. case mval of
  28. Nothing -> deleteSession key
  29. Just val -> setSession key val
  30. liftIO $ print (key, mval)
  31. redirect HomeR
  32. instance Yesod App where
  33. -- Make the session timeout 1 minute so that it's easier to play with
  34. makeSessionBackend _ = do
  35. backend <- defaultClientSessionBackend 1 "keyfile.aes"
  36. return $ Just backend
  37. instance RenderMessage App FormMessage where
  38. renderMessage _ _ = defaultFormMessage
  39. main :: IO ()
  40. main = warp 3000 App

Messages

One usage of sessions previously alluded to is messages. They come to solve a common problem in web development: the user performs a POST request, the web app makes a change, and then the web app wants to simultaneously redirect the user to a new page and send the user a success message. (This is known as Post/Redirect/Get.)

Yesod provides a pair of functions to enable this workflow: setMessage stores a value in the session, and getMessage both reads the value most recently put into the session, and clears the old value so it is not displayed twice.

It is recommended to have a call to getMessage in defaultLayout so that any available message is shown to a user immediately, without having to add getMessage calls to every handler.

  1. {-# LANGUAGE MultiParamTypeClasses #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. {-# LANGUAGE QuasiQuotes #-}
  4. {-# LANGUAGE TemplateHaskell #-}
  5. {-# LANGUAGE TypeFamilies #-}
  6. import Yesod
  7. data App = App
  8. mkYesod "App" [parseRoutes|
  9. / HomeR GET
  10. /set-message SetMessageR POST
  11. |]
  12. instance Yesod App where
  13. defaultLayout widget = do
  14. pc <- widgetToPageContent widget
  15. mmsg <- getMessage
  16. withUrlRenderer
  17. [hamlet|
  18. $doctype 5
  19. <html>
  20. <head>
  21. <title>#{pageTitle pc}
  22. ^{pageHead pc}
  23. <body>
  24. $maybe msg <- mmsg
  25. <p>Your message was: #{msg}
  26. ^{pageBody pc}
  27. |]
  28. instance RenderMessage App FormMessage where
  29. renderMessage _ _ = defaultFormMessage
  30. getHomeR :: Handler Html
  31. getHomeR = defaultLayout
  32. [whamlet|
  33. <form method=post action=@{SetMessageR}>
  34. My message is: #
  35. <input type=text name=message>
  36. <button>Go
  37. |]
  38. postSetMessageR :: Handler ()
  39. postSetMessageR = do
  40. msg <- runInputPost $ ireq textField "message"
  41. setMessage $ toHtml msg
  42. redirect HomeR
  43. main :: IO ()
  44. main = warp 3000 App

Initial page load, no message

Sessions - 图1

New message entered in text box

Sessions - 图2

After form submit, message appears at top of page

Sessions - 图3

After refresh, the message is cleared

Sessions - 图4

Ultimate Destination

Not to be confused with a horror film, ultimate destination is a technique originally developed for Yesod’s authentication framework, but which has more general usefulness. Suppose a user requests a page that requires authentication. If the user is not yet logged in, you need to send him/her to the login page. A well-designed web app will then send them back to the first page they requested. That’s what we call the ultimate destination.

redirectUltDest sends the user to the ultimate destination set in his/her session, clearing that value from the session. It takes a default destination as well, in case there is no destination set. For setting the session, there are three options:

  • setUltDest sets the destination to the given URL, which can be given either as a textual URL or a type-safe URL.

  • setUltDestCurrent sets the destination to the currently requested URL.

  • setUltDestReferer sets the destination based on the Referer header (the page that led the user to the current page).

Additionally, there is the clearUltDest function, to drop the ultimate destination value from the session if present.

Let’s look at a small sample app. It will allow the user to set his/her name in the session, and then tell the user his/her name from another route. If the name hasn’t been set yet, the user will be redirected to the set name page, with an ultimate destination set to come back to the current page.

  1. {-# LANGUAGE MultiParamTypeClasses #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. {-# LANGUAGE QuasiQuotes #-}
  4. {-# LANGUAGE TemplateHaskell #-}
  5. {-# LANGUAGE TypeFamilies #-}
  6. import Yesod
  7. data App = App
  8. mkYesod "App" [parseRoutes|
  9. / HomeR GET
  10. /setname SetNameR GET POST
  11. /sayhello SayHelloR GET
  12. |]
  13. instance Yesod App
  14. instance RenderMessage App FormMessage where
  15. renderMessage _ _ = defaultFormMessage
  16. getHomeR :: Handler Html
  17. getHomeR = defaultLayout
  18. [whamlet|
  19. <p>
  20. <a href=@{SetNameR}>Set your name
  21. <p>
  22. <a href=@{SayHelloR}>Say hello
  23. |]
  24. -- Display the set name form
  25. getSetNameR :: Handler Html
  26. getSetNameR = defaultLayout
  27. [whamlet|
  28. <form method=post>
  29. My name is #
  30. <input type=text name=name>
  31. . #
  32. <input type=submit value="Set name">
  33. |]
  34. -- Retrieve the submitted name from the user
  35. postSetNameR :: Handler ()
  36. postSetNameR = do
  37. -- Get the submitted name and set it in the session
  38. name <- runInputPost $ ireq textField "name"
  39. setSession "name" name
  40. -- After we get a name, redirect to the ultimate destination.
  41. -- If no destination is set, default to the homepage
  42. redirectUltDest HomeR
  43. getSayHelloR :: Handler Html
  44. getSayHelloR = do
  45. -- Lookup the name value set in the session
  46. mname <- lookupSession "name"
  47. case mname of
  48. Nothing -> do
  49. -- No name in the session, set the current page as
  50. -- the ultimate destination and redirect to the
  51. -- SetName page
  52. setUltDestCurrent
  53. setMessage "Please tell me your name"
  54. redirect SetNameR
  55. Just name -> defaultLayout [whamlet|<p>Welcome #{name}|]
  56. main :: IO ()
  57. main = warp 3000 App

Summary

Sessions are the primary means by which we bypass the statelessness imposed by HTTP. We shouldn’t consider this an escape hatch to perform whatever actions we want: statelessness in web applications is a virtue, and we should respect it whenever possible. However, there are specific cases where it is vital to retain some state.

The session API in Yesod is very simple. It provides a key-value store, and a few convenience functions built on top for common use cases. If used properly, with small payloads, sessions should be an unobtrusive part of your web development.