module Session.Key
( SessionKey (..)
, SessionKeyManager (..)
, makeSessionKeyManager
, sessionKeyToCookieValue
, sessionKeyFromCookieValue
)
where
import Internal.Prelude
import Base64 (decodeBase64, encodeBase64)
import Data.ByteString.Char8 qualified as BS8
import Data.Text qualified as T
import Data.Text.Encoding (decodeUtf8', encodeUtf8)
import Randomization
data SessionKeyManager m = SessionKeyManager
{ new :: m SessionKey
-- ^ Generate a new session key
--
-- In a production setting, it is critical that this action be
-- thread-safe and produce a securely random result.
, check :: SessionKey -> Bool
-- ^ Validate that a text is something that plausibly could have
-- been generated by 'new'.
}
-- | Secret value that is sent to and subsequently furnished by
-- the client to identify the session
newtype SessionKey = SessionKey {text :: Text}
deriving newtype (Eq, Ord, Show)
makeSessionKeyManager :: Monad m => Randomization m -> SessionKeyManager m
makeSessionKeyManager (Randomization generateRandomBytes) =
let
new =
SessionKey
. encodeBase64
<$> generateRandomBytes keyLengthInBytes
check (SessionKey text) =
T.length text
== keyLengthAsText
&& either
(const False)
((== keyLengthInBytes) . BS8.length)
(decodeBase64 text)
in
SessionKeyManager {new, check}
-- We generate 18-byte session keys. This number is rather arbitrary.
keyLengthInBytes :: Integral a => a
keyLengthInBytes = 18
-- 18 bytes in base64 encoding ends up being a text 24 characters
keyLengthAsText :: Integral a => a
keyLengthAsText = 24
sessionKeyToCookieValue :: SessionKey -> ByteString
sessionKeyToCookieValue = (.text) >>> encodeUtf8
sessionKeyFromCookieValue :: ByteString -> Maybe SessionKey
sessionKeyFromCookieValue v =
decodeUtf8' v & either (const Nothing) Just <&> SessionKey