Skip to content
Al Zohali edited this page May 7, 2017 · 15 revisions

servant-auth-cookie adds support for authentication via cookies into servant framework. It was inspired by Michael Snoyman's library client-session and based on ideas of the paper "A Secure Cookie Protocol" by Alex Liu et al. Session data is stored in cookies in encrypted form, so the client is unable to read nor forge it.

Demo

Library comes with an example that uses the most of the API. It might help you to understand how to use the library.

To run the example enable flag build-examples and run executable example:

cabal configure -f build-example -f servant91
cabal run example

(Note: it's recommended to use >= 0.9.1.* versions of servant for more features will be enabled.)

This will launch local server at 8080 port. It's a simple three/four-paged web site that will show the private page only if a correct cookie is presented. For valid accounts see usersDB list in example/AuthAPI.hs.

Getting started

Session data

To store and manipulate data in our cookies we need to define a datatype. We will use it to identify users and verify their authenticity:

data Account = Account
  { accUid       :: Int
  , _accUsername :: String
  , _accPassword :: String
  } deriving (Show, Eq, Generic)

instance Serialize Account

Instance of Data.Serialize.Serialize is required as we will transform it into binary form and vice versa.

Now we are going to say, that we will use it as a payload in our cookies:

type instance AuthCookieData = Account

Endpoints

Every endpoint that reads or modifies cookies should be wrapped in Cookied type. Every endpoint that is protected by the authentication should be prepended by AuthProtect "cookie-auth" type. A protected endpoint indirectly reads and modifies cookies, so such endpoint should always be wrapped in Cookied.

For example:

ExampleAPI = ...
  :<|> "login" :> ReqBody '[FormUrlEncoded] LoginForm :> Post '[HTML] (Cookied Markup)
  :<|> "logout" :> Get '[HTML] (Cookied Markup)
  :<|> "private" :> AuthProtect "cookie-auth" :> Get '[HTML] (Cookied Markup)

Case for servant < 0.9

Every endpoint that modifies cookies should be specified with Set-Cookie header. Read-only endpoints do not require any changes. Protected endpoints do not modify cookies, so again, no changes.

ExampleAPI = ...
  :<|> "login"
       :> ReqBody '[FormUrlEncoded] LoginForm
       :> Post '[HTML] (Headers '[Header "Set-Cookie" EncryptedSession] Markup)
  :<|> "logout"
       :> Get '[HTML] (Headers '[Header "Set-Cookie" EncryptedSession] Markup)
  :<|> "private" :> AuthProtect "cookie-auth" :> Get '[HTML] Markup

Handlers

To set cookies and to read them we use functions addSession and cookied respectively.

Both functions have the same first three arguments (they will be explained later) and it's convenient to define shorter versions in where clause, i.e.:

addSession' = addSession settings rs sks
cookied' = cookied settings rs sks

addSession' takes two arguments -- value of type AuthCookieData (that will go into cookies) and what will be returned to a client.

cookied' is a wrapper, that takes handler with extra argument of type AuthCookieData and returns a handler. Behind the scenes it might change cookies metadata.

They can be used like this:

serveLogin loginData = if check loginData
  then addSession' (Account 1 "admin" "password") welcomePage
  else throwError err401

serveProfile = cookied' serveProfile'
serveProfile' (Account i u _) = return $ profilePage i u

Case for servant < 0.9

For earlier versions of servant there is no cookied function. Instead addSession can be used (if changing the cookies is required) or the following construction (if the handler doesn't change the cookies):

  serveProfile = return . serveProfile . wmData

Putting all together

We still need to provide some additional parameters before we will be able to define the application. To avoid bloating this section, let's use the simplest values:

let settings = (def :: AuthCookieSettings) {acsCookieFlags = ["HttpOnly"]}
let sks = mkPersistentServerKey "0123456789abcdef"
rs <- mkRandomSource drgNew 1000

let app = serveWithContext
  (Proxy :: Proxy ExampleAPI)
  ((authHandler settings sks) :. EmptyContext)
  (server settings rs sks)

Server keys management

TODO

Security considerations

TODO

Clone this wiki locally