packages feed

apiary-clientsession 1.1.0 → 1.2.0

raw patch · 4 files changed

+119/−308 lines, 4 filesdep +apiary-sessiondep +cerealdep +unix-compatdep −base64-bytestringdep −binarydep −blaze-htmldep ~apiarydep ~apiary-cookiedep ~timePVP ok

version bump matches the API change (PVP)

Dependencies added: apiary-session, cereal, unix-compat, vault

Dependencies removed: base64-bytestring, binary, blaze-html, cprng-aes, crypto-random, directory, http-types, template-haskell

Dependency ranges changed: apiary, apiary-cookie, time

API changes (from Hackage documentation)

- Web.Apiary.ClientSession: KeyByteString :: ByteString -> KeySource
- Web.Apiary.ClientSession: KeyFile :: FilePath -> KeySource
- Web.Apiary.ClientSession: SessionConfig :: KeySource -> DiffTime -> Maybe ByteString -> Maybe ByteString -> Bool -> Bool -> (forall exts prms m. MonadIO m => ActionT exts prms m ()) -> Maybe ByteString -> ByteString -> Either HeaderName ByteString -> Int -> SessionConfig
- Web.Apiary.ClientSession: angularXsrfCookieName :: SessionConfig -> Maybe ByteString
- Web.Apiary.ClientSession: checkToken :: (MonadIO actM, Has Session exts) => ApiaryT exts prms actM m () -> ApiaryT exts prms actM m ()
- Web.Apiary.ClientSession: csrfToken :: (Has Session exts, MonadIO m) => ActionT exts prms m ByteString
- Web.Apiary.ClientSession: csrfTokenCheckingName :: SessionConfig -> Either HeaderName ByteString
- Web.Apiary.ClientSession: csrfTokenCookieName :: SessionConfig -> ByteString
- Web.Apiary.ClientSession: csrfTokenLength :: SessionConfig -> Int
- Web.Apiary.ClientSession: data KeySource
- Web.Apiary.ClientSession: data Session
- Web.Apiary.ClientSession: data SessionConfig
- Web.Apiary.ClientSession: embedDefaultKeyConfig :: ExpQ
- Web.Apiary.ClientSession: embedKeyConfig :: FilePath -> ExpQ
- Web.Apiary.ClientSession: getSessionConfig :: (Has Session exts, Monad m) => ActionT exts prms m SessionConfig
- Web.Apiary.ClientSession: initSession :: MonadIO m => SessionConfig -> Initializer' m Session
- Web.Apiary.ClientSession: session :: (MonadIO actM, Strategy w, Has Session exts, KnownSymbol k, NotMember k prms, Query a) => proxy k -> w a -> ApiaryT exts (SNext w k a prms) actM m () -> ApiaryT exts prms actM m ()
- Web.Apiary.ClientSession: sessionDomain :: SessionConfig -> Maybe ByteString
- Web.Apiary.ClientSession: sessionHttpOnly :: SessionConfig -> Bool
- Web.Apiary.ClientSession: sessionKeySource :: SessionConfig -> KeySource
- Web.Apiary.ClientSession: sessionMaxAge :: SessionConfig -> DiffTime
- Web.Apiary.ClientSession: sessionPath :: SessionConfig -> Maybe ByteString
- Web.Apiary.ClientSession: sessionSecure :: SessionConfig -> Bool
- Web.Apiary.ClientSession: sessionTimeoutAction :: SessionConfig -> forall exts prms m. MonadIO m => ActionT exts prms m ()
- Web.Apiary.ClientSession: setSession :: (Has Session exts, MonadIO m) => ByteString -> ByteString -> ActionT exts prms m ()
- Web.Apiary.ClientSession: setSessionWith :: (Has Session exts, MonadIO m) => SessionConfig -> ByteString -> ByteString -> ActionT exts prms m ()
+ Web.Apiary.Session.ClientSession: ClientSessionConfig :: ByteString -> Maybe ByteString -> Maybe ByteString -> Bool -> Bool -> Maybe DiffTime -> IO Key -> ClientSessionConfig
+ Web.Apiary.Session.ClientSession: csCookieDomain :: ClientSessionConfig -> Maybe ByteString
+ Web.Apiary.Session.ClientSession: csCookieHttpOnly :: ClientSessionConfig -> Bool
+ Web.Apiary.Session.ClientSession: csCookieName :: ClientSessionConfig -> ByteString
+ Web.Apiary.Session.ClientSession: csCookiePath :: ClientSessionConfig -> Maybe ByteString
+ Web.Apiary.Session.ClientSession: csCookieSecure :: ClientSessionConfig -> Bool
+ Web.Apiary.Session.ClientSession: csSessionKey :: ClientSessionConfig -> IO Key
+ Web.Apiary.Session.ClientSession: csTTL :: ClientSessionConfig -> Maybe DiffTime
+ Web.Apiary.Session.ClientSession: data ClientSessionConfig
+ Web.Apiary.Session.ClientSession: initClientSession :: (MonadIO m, Serialize sess) => proxy sess -> ClientSessionConfig -> Initializer' m (Session sess m)
+ Web.Apiary.Session.ClientSession: instance (Serialize sess, MonadIO m) => SessionBackend (ClientSessionBackend sess m) sess m
+ Web.Apiary.Session.ClientSession: instance Default ClientSessionConfig

Files

apiary-clientsession.cabal view
@@ -1,14 +1,12 @@ name:                apiary-clientsession-version:             1.1.0+version:             1.2.0 synopsis:            clientsession support for apiary web framework. description:   examples:     .-    <https://github.com/philopon/apiary/blob/master/examples/csrf.hs>+    <https://github.com/philopon/apiary/blob/master/examples/session.hs>     .     <https://github.com/philopon/apiary/blob/master/examples/auth.hs>-    .-    <https://github.com/philopon/apiary/blob/master/examples/embed_key.hs> license:             MIT license-file:        LICENSE author:              HirotomoMoriwaki<philopon.dependence@gmail.com>@@ -23,25 +21,19 @@ cabal-version:       >=1.10  library-  exposed-modules:     Web.Apiary.ClientSession-  other-modules:       Web.Apiary.ClientSession.Internal+  exposed-modules:     Web.Apiary.Session.ClientSession   build-depends:       base               >=4.6   && <4.8-                     , template-haskell-                     , clientsession      >=0.9   && <0.10-                     , apiary             >=1.1   && <1.2-                     , apiary-cookie      >=1.1   && <1.2--                     , directory          >=1.2   && <1.3-                     , crypto-random      >=0.0   && <0.1-                     , cprng-aes          >=0.5   && <0.6+                     , apiary             >=1.2   && <1.3+                     , apiary-cookie      >=1.2   && <1.3+                     , apiary-session     >=1.2   && <1.3 -                     , binary             >=0.7   && <0.8+                     , clientsession      >=0.9   && <0.10+                     , vault              >=0.3   && <0.4+                     , cereal             >=0.4   && <0.5                      , bytestring         >=0.10  && <0.11-                     , base64-bytestring  >=1.0   && <1.1-                     , time               >=1.4   && <1.6+                     , time               >=1.4   && <1.5+                     , unix-compat        >=0.4   && <0.5                      , data-default-class >=0.0   && <0.1-                     , http-types         >=0.8   && <0.9-                     , blaze-html         >=0.7   && <0.8    hs-source-dirs:      src   ghc-options:         -O2 -Wall
− src/Web/Apiary/ClientSession.hs
@@ -1,71 +0,0 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE Rank2Types #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE DataKinds #-}--module Web.Apiary.ClientSession-    ( I.Session-    -- * config-    , I.SessionConfig(..), I.KeySource(..)-    , I.embedKeyConfig, I.embedDefaultKeyConfig-    -- * initializer-    , initSession-    -- * getter-    , getSessionConfig-    -- * setter-    , setSession-    , csrfToken-    -- ** with sessionConfig-    , setSessionWith-    -- * filter-    , I.session-    , I.checkToken-    -- * Reexport-    -- | deleteCookie-    , module Web.Apiary.Cookie-    ) where--import Web.Apiary--import Data.Apiary.Extension-import Data.Apiary.Compat--import Control.Monad.Apiary.Action-import qualified Web.Apiary.ClientSession.Internal as I-import Web.Apiary.Cookie (deleteCookie)-import qualified Data.ByteString as S--initSession :: MonadIO m => I.SessionConfig -> Initializer' m I.Session-initSession c = initializer' $ I.makeSession c--setSession :: (Has I.Session exts, MonadIO m)-           => S.ByteString -> S.ByteString-           -> ActionT exts prms m ()-setSession k v = do-    sess <- getExt (Proxy :: Proxy I.Session)-    I.setSession sess k v--getSessionConfig :: (Has I.Session exts, Monad m)-                 => ActionT exts prms m I.SessionConfig-getSessionConfig = do-    sess <- getExt (Proxy :: Proxy I.Session)-    return $ I.sessionConfig sess--setSessionWith :: (Has I.Session exts, MonadIO m)-               => I.SessionConfig-               -> S.ByteString -> S.ByteString-               -> ActionT exts prms m ()-setSessionWith cfg k v = do-    sess <- getExt (Proxy :: Proxy I.Session)-    I.setSession sess { I.sessionConfig = cfg } k v----- | create crypto random (generate random by AES CTR(cprng-aes package) and encode by base64),------ set it client session cookie, set XSRF-TOKEN header(when Just angularXsrfCookieName),------ and return value. since 0.9.0.0.-csrfToken :: (Has I.Session exts, MonadIO m) => ActionT exts prms m S.ByteString-csrfToken = getExt (Proxy :: Proxy I.Session) >>= I.csrfToken
− src/Web/Apiary/ClientSession/Internal.hs
@@ -1,218 +0,0 @@-{-# LANGUAGE Rank2Types #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE TypeFamilies #-}--module Web.Apiary.ClientSession.Internal where--import Language.Haskell.TH--import System.Directory--import "crypto-random" Crypto.Random-import Crypto.Random.AESCtr--import Control.Monad-import Control.Applicative-import Control.Monad.Apiary.Filter-import Control.Monad.Apiary.Action--import Web.Apiary.Wai-import Web.Apiary-import Web.Apiary.Cookie-import Web.ClientSession -import qualified Network.HTTP.Types as HTTP--import Data.Apiary.Compat-import Data.Apiary.Param-import Data.Apiary.Extension-import Data.String-import Data.Maybe-import Data.Monoid-import Data.Time-import Data.Default.Class-import Data.Binary-import Data.IORef--import Text.Blaze.Html-import qualified Data.ByteString.Base64 as Base64-import qualified Data.ByteString as S-import qualified Data.ByteString.Char8 as SC-import qualified Data.ByteString.Lazy as L--data Session = Session-    { sessionKey    :: Key-    , tokenGen      :: IORef AESRNG-    , sessionConfig :: SessionConfig-    }-instance Extension Session--data KeySource-    = KeyFile       FilePath-    | KeyByteString S.ByteString--instance IsString KeySource where-    fromString = KeyByteString . fromString---- | generate and embed key at compile time. since 0.13.2.------ This function embed as SessionsessionConfig with default sessionConfig. so you can sessionConfigure it.--- but DON'T sessionConfigure sessionKey.------ this function is convenient when create heroku project.--- --- @--- embedsessionConfig = $embedDefaultKeysessionConfig { csrfTokenCookieName = \"foo\" }--- @-embedKeyConfig :: FilePath -> ExpQ-embedKeyConfig keyfile = do-    bs <- runIO $ do-        exists <- doesFileExist keyfile-        if exists-            then do -                b <- S.readFile keyfile-                case initKey b of-                    Left  _ -> newKey-                    Right _ -> return b-        else newKey-    let s = stringE $ SC.unpack bs-    [| def { sessionKeySource = KeyByteString $s } |]-  where-    newKey = do-        (bs, _) <- randomKey-        S.writeFile keyfile bs-        return bs--embedDefaultKeyConfig :: ExpQ-embedDefaultKeyConfig = embedKeyConfig defaultKeyFile--data SessionConfig = SessionConfig-    { sessionKeySource      :: KeySource-    , sessionMaxAge         :: DiffTime-    , sessionPath           :: Maybe S.ByteString-    , sessionDomain         :: Maybe S.ByteString-    , sessionHttpOnly       :: Bool-    , sessionSecure         :: Bool--    , sessionTimeoutAction  :: forall exts prms m. MonadIO m => ActionT exts prms m ()--    , angularXsrfCookieName :: Maybe S.ByteString-    , csrfTokenCookieName   :: S.ByteString--    , csrfTokenCheckingName :: Either HTTP.HeaderName S.ByteString-    , csrfTokenLength       :: Int-    }--defaultCheckTokenFailAction :: Monad actM => ActionT exts prms actM ()-defaultCheckTokenFailAction = do-    reset-    status status401-    bytes "session timeout\n"-    stop--instance Default SessionConfig where-    def = SessionConfig-        (KeyFile defaultKeyFile) (24 * 60 * 60) Nothing Nothing True True-        defaultCheckTokenFailAction Nothing "_token" (Right "_token") 40--makeSession :: MonadIO m => SessionConfig -> m Session-makeSession cfg@SessionConfig{..} = do-    k <- liftIO $ case sessionKeySource of-        KeyFile       f -> getKey f-        KeyByteString s -> either fail return $ initKey s-    p <- liftIO $ makeSystem >>= newIORef-    let sess = Session k p cfg-    return sess--newtype BinUTCTime = BinUTCTime { getUTCTime :: UTCTime }--instance Binary BinUTCTime where-    put (BinUTCTime t) = do-        put . toModifiedJulianDay $ utctDay t-        put . toRational $ utctDayTime t-    get = do-        d <- ModifiedJulianDay <$> get-        t <- fromRational      <$> get-        return . BinUTCTime $ UTCTime d t--mkSessionCookie :: SessionConfig -> Key -> S.ByteString -> S.ByteString -> IO SetCookie-mkSessionCookie conf skey k v = do-    t <- getCurrentTime-    let expire = addUTCTime (realToFrac $ sessionMaxAge conf) t-    v' <- encryptIO skey $ L.toStrict $ encode (BinUTCTime expire, v)-    return def { setCookieName     = k-               , setCookieValue    = v'-               , setCookiePath     = sessionPath conf-               , setCookieExpires  = Just expire-               , setCookieMaxAge   = Just (sessionMaxAge conf)-               , setCookieDomain   = sessionDomain conf-               , setCookieHttpOnly = sessionHttpOnly conf-               , setCookieSecure   = sessionSecure conf-               }--getSessionValue :: Session -> UTCTime -- ^ current time-                -> S.ByteString -                -> Maybe S.ByteString-getSessionValue Session{sessionKey = k} c s = decrypt k s >>= \s' -> case decodeOrFail (L.fromStrict s') of-        Right (_, _, (BinUTCTime t, v)) -> if c < t then Just v else Nothing-        _ -> Nothing--setSession :: MonadIO m => Session -> S.ByteString -> S.ByteString -> ActionT exts prms m ()-setSession sess k v = do-    s <- liftIO $ mkSessionCookie (sessionConfig sess) (sessionKey sess) k v-    setCookie s--newToken :: Int -> IORef AESRNG -> IO S.ByteString-newToken len gen = do-    atomicModifyIORef' gen (\rng -> swap $ withRandomBytes rng len Base64.encode)-  where -    swap (a,b) = (b,a)--csrfToken :: MonadIO m => Session -> ActionT exts prms m S.ByteString-csrfToken Session{..} = do-    tok <- liftIO $ newToken (csrfTokenLength sessionConfig) tokenGen -    sc <- liftIO $ mkSessionCookie sessionConfig sessionKey (csrfTokenCookieName sessionConfig) tok-    setCookie sc-    maybe (return ()) (setCookie . ngCookie sc tok) (angularXsrfCookieName sessionConfig)-    return tok-  where-    ngCookie sc tok k = sc { setCookieName     = k-                           , setCookieValue    = tok-                           , setCookieHttpOnly = False-                           }--session :: (MonadIO actM, Strategy w, Has Session exts, KnownSymbol k, NotMember k prms, Query a)-        => proxy k -> w a -> ApiaryT exts (SNext w k a prms) actM m () -> ApiaryT exts prms actM m ()-session k p = focus (DocPrecondition $ toHtml (symbolVal k) <> " session cookie required") $ do-    sess <- getExt (Proxy :: Proxy Session)-    t    <- liftIO getCurrentTime-    c    <- map (readQuery . getSessionValue sess t . snd) .-        filter ((SC.pack (symbolVal k) ==) . fst) . cookie' <$> getRequest-    strategy p k c =<< getParams--checkToken :: (MonadIO actM, Has Session exts)-           => ApiaryT exts prms actM m ()-           -> ApiaryT exts prms actM m ()-checkToken = focus (DocPrecondition "CSRF token required") $ do-    sess@Session{..} <- getExt (Proxy :: Proxy Session)-    qs <- getQueryParams-    r  <- getRequest-    p  <- getReqBodyParams--    t <- liftIO getCurrentTime-    let stok = getSessionValue sess t =<< -               lookup (csrfTokenCookieName sessionConfig) (cookie' r)-    guard (isJust stok)-    -    qtok <- return . join $ case csrfTokenCheckingName sessionConfig of-        Right name -> lookup name $ reqParams pByteString qs p []-        Left name  -> lookup name $ map (\(k,v) -> (k, Just v)) $ requestHeaders r-    guard (isJust qtok)--    if qtok == stok then getParams else sessionTimeoutAction sessionConfig >> mzero
+ src/Web/Apiary/Session/ClientSession.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE LambdaCase #-}++module Web.Apiary.Session.ClientSession+    ( ClientSessionConfig(..)+    , initClientSession+    , module Web.Apiary.Session+    ) where++import Web.Apiary(MonadIO(..))+import Web.Apiary.Session+import Web.Apiary.Session.Internal+    (Session(Session), SessionBackend(backendMiddleware', genBackendModify))+import Web.Apiary.Cookie(getCookies, deleteCookie, SetCookie(..), setCookie)+import Data.Apiary.Extension(Initializer', initializer')++import Control.Monad.Apiary.Action(insertVault, lookupVault, deleteVault)+import Control.Applicative ((<$))++import Foreign.C.Types(CTime(..))++import System.PosixCompat.Time(epochTime)++import Data.Time(DiffTime, addUTCTime)+import Data.Time.Clock.POSIX(posixSecondsToUTCTime)+import qualified Data.ByteString as S+import qualified Data.Serialize as Serialize+import qualified Data.Vault.Lazy as Vault+import Data.Default.Class(Default(def))++import qualified Web.ClientSession as CS++data ClientSessionConfig = ClientSessionConfig+    { csCookieName     :: S.ByteString+    , csCookiePath     :: Maybe S.ByteString+    , csCookieDomain   :: Maybe S.ByteString+    , csCookieHttpOnly :: Bool+    , csCookieSecure   :: Bool+    , csTTL            :: Maybe DiffTime+    , csSessionKey     :: IO CS.Key+    }++data ClientSessionBackend sess (m :: * -> *) = ClientSessionBackend+    { clientSessionEncryptKey :: CS.Key+    , clientSessionVaultKey   :: Vault.Key sess+    , clientSessionConfig     :: ClientSessionConfig+    }++instance Default ClientSessionConfig where+    def = ClientSessionConfig "_sess" (Just "/") Nothing True True+        (Just $ 7 * 24 * 60 * 60) (liftIO CS.getDefaultKey)++initClientSession :: (MonadIO m, Serialize.Serialize sess)+                  => proxy sess -- ^ session type to initialize.+                  -> ClientSessionConfig+                  -> Initializer' m (Session sess m)+initClientSession _ cfg = initializer' $ do+    eKey <- liftIO $ csSessionKey cfg+    vKey <- liftIO Vault.newKey+    return $ Session (ClientSessionBackend eKey vKey cfg)++instance (Serialize.Serialize sess, MonadIO m) => SessionBackend (ClientSessionBackend sess m) sess m where+    backendMiddleware' ClientSessionBackend{clientSessionConfig = ClientSessionConfig{..}, ..} m = do+        cs  <- getCookies+        mbNow <- case lookup csCookieName cs >>= CS.decrypt clientSessionEncryptKey >>=+            either (const Nothing) Just . Serialize.decode of+                Nothing     -> return Nothing+                Just (t, v) -> do+                    case csTTL of+                        Nothing  -> Nothing <$ insertVault clientSessionVaultKey v+                        Just ttl -> do+                            CTime now <- liftIO epochTime+                            if t + round ttl < now+                            then return (Just now)+                            else Just now <$ insertVault clientSessionVaultKey v+        m+        lookupVault clientSessionVaultKey >>= \case+            Nothing -> deleteCookie csCookieName+            Just v  -> do+                now <- maybe (liftIO epochTime >>= \(CTime i) -> return i) return mbNow+                v'  <- liftIO . CS.encryptIO clientSessionEncryptKey $ Serialize.encode (now, v)+                let cCookie = def+                        { setCookieName     = csCookieName+                        , setCookieValue    = v'+                        , setCookiePath     = csCookiePath+                        , setCookieDomain   = csCookieDomain+                        , setCookieHttpOnly = csCookieHttpOnly+                        , setCookieSecure   = csCookieSecure+                        }+                case csTTL of+                    Nothing -> setCookie cCookie+                    Just d  -> do+                        let now' = posixSecondsToUTCTime $ realToFrac now+                        setCookie cCookie+                            { setCookieMaxAge  = Just d+                            , setCookieExpires = Just (addUTCTime (realToFrac d) now')+                            }++    genBackendModify ClientSessionBackend{clientSessionConfig = ClientSessionConfig{..}, ..} f = do+        sess       <- lookupVault clientSessionVaultKey+        (sess', a) <- f sess+        maybe (deleteVault clientSessionVaultKey) (insertVault clientSessionVaultKey) sess'+        return a