-- | This module exports tools for safely storing encrypted data on client-side -- cookies through "Network.Wai". -- -- This module is designed to be imported as follows: -- -- @ -- import qualified "Wai.CryptoCookie" -- @ -- -- One example of how to obtain a new "Network.Wai".'Network.Wai.Middleware', -- -- @ -- do (__middleware__, __lookup__) <- do -- key <- "Wai.CryptoCookie".'autoKeyFileBase16' \"\/run\/my-cookie-encryption-key\" -- "Wai.CryptoCookie".'middleware' ("Wai.CryptoCookie".'defaultConfig' key) -- @ -- -- The obtained @__middleware__@ shall be applied to your -- "Network.Wai".'Wai.Application'. -- -- The obtained @__lookup__@ function can be used to obtain the 'CryptoCookie' -- associated with each 'Wai.Request'. It returns 'Nothing' if this particular -- @__middleware__@ was not used on the given 'Wai.Request'. -- -- Finally, interact with the 'CryptoCookie' data using 'get' or 'set'. -- -- == Do I store session data on the client or on the server? -- -- It's not so much about /where/ to store the session data, but about /how/ to -- store it and /how/ to expire it. Here are some ideas. But please, do your -- own research. -- -- 1. __Data on server, identifier on both__: In this approach, all the data is -- stored on the server. On the 'CryptoCookie', simply 'set' a unique session -- identifier and later 'get' it back and use it to find the associated session -- data on your server-side database. In order to expire the session, all the -- server have to do is remove this session identifier from its database. -- /Choose this option unless you know what you are doing,/ -- /it doesn't require you to plan ahead too much./ -- -- 2. __Data on the client, identifier on both__: In this approach, all the -- data and session identifier is stored on the 'CryptoCookie'. On your -- server-side database, store the session identifier and a timestamp -- representing its creation time or last session activity time. -- Before accepting the session data from the 'CryptoCookie' as valid, check -- that the session identifier exists in your database, and that the time since -- the timestamp is acceptable. This approach is simpler on your server-side -- database, but it can lead to more network traffic, and schema migrations for -- session data will be complex if you care about backwards compatibility -- with currently active sessions. -- -- 3. __Everything on the client__: You can store everything in the -- 'CryptoCookie'. However, you will be more suceptible to /replay attacks/ -- because you won't have control over session expiration beyond comparing the -- current time against the session creation timestamp or last activity -- timestamp previously set in the session data. You can force all the -- existing sessions to “expire” by rotating your encryption 'Key'. Also, this -- approach can lead to more network traffic, and schema migrations for session -- data will be complex if you care about backwards compatibility with -- currently active sessions. module Wai.CryptoCookie ( -- * Cookie data CryptoCookie , get , set -- * Middleware , middleware , defaultConfig , Config (..) , autoKeyFileBase16 , readKeyFileBase16 ) where import Data.Aeson qualified as Ae import Web.Cookie (SetCookie (..), defaultSetCookie, sameSiteLax) import Wai.CryptoCookie.Encoding import Wai.CryptoCookie.Encryption import Wai.CryptoCookie.Encryption.AEAD_AES_128_GCM_SIV () import Wai.CryptoCookie.Encryption.AEAD_AES_256_GCM_SIV () import Wai.CryptoCookie.Middleware -- | Default 'Config': -- -- * 'Encoding' is 'aeson'. -- -- * 'Encryption' scheme is the nonce-misuse resistant @AEAD_AES_256_GCM_SIV@ -- as defined in in <https://tools.ietf.org/html/rfc8452 RFC 8452>. -- -- As an AEAD encryption scheme, you can be confident that a successfully -- decrypted cookie could only have been encrypted by the same -- 'Key'. This makes this encryption scheme suitable for -- storing user session authentication identifiers generated by the server. -- -- * Cookie name is @SESSION@. -- -- * @HttpOnly@: yes -- -- * @Max-Age@: 16 hours -- -- * @Path@: @\/@ -- -- * @SameSite@: @Lax@ -- -- * @Secure@: yes -- -- * @Domain@: not set defaultConfig :: (Ae.FromJSON a, Ae.ToJSON a) => Key "AEAD_AES_256_GCM_SIV" -- ^ Consider using 'autoKeyFileBase16' or -- 'readKeyFileBase16' for safely reading a 'Key' from a -- 'FilePath'. Alternatively, if you have the base-16 representation of the -- 'Key' in JSON configuration, you coulud use -- 'Data.Aeson.FromJSON'. -> Config a defaultConfig :: forall a. (FromJSON a, ToJSON a) => Key "AEAD_AES_256_GCM_SIV" -> Config a defaultConfig Key "AEAD_AES_256_GCM_SIV" key = Config { Key "AEAD_AES_256_GCM_SIV" key :: Key "AEAD_AES_256_GCM_SIV" key :: Key "AEAD_AES_256_GCM_SIV" key , encoding :: Encoding a encoding = Encoding a forall a. (FromJSON a, ToJSON a) => Encoding a aeson , setCookie :: SetCookie setCookie = SetCookie defaultSetCookie { setCookieDomain = Nothing , setCookieExpires = Nothing , setCookieHttpOnly = True , setCookieMaxAge = Just (16 * 60 * 60) , setCookieName = "SESSION" , setCookiePath = Just "/" , setCookieSameSite = Just sameSiteLax , setCookieSecure = True , setCookieValue = error "setCookieValue" } }