-- | 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"
-- @
--
-- Use 'middleware' to obtain a function to allow an 'Wai.Application' to
-- interact with a 'CryptoCookie' using 'get', 'set', 'delete' and 'keep'.
--
-- == 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
, delete
, keep
-- * 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 key =
Config
{ key
, encoding = aeson
, 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"
}
}