packages feed

apiary-clientsession 0.8.0.0 → 0.9.0.0

raw patch · 4 files changed

+188/−66 lines, 4 filesdep +base64-bytestringdep +binarydep +cprng-aesdep −mtldep ~apiarydep ~apiary-cookiePVP ok

version bump matches the API change (PVP)

Dependencies added: base64-bytestring, binary, cprng-aes, crypto-random, http-types

Dependencies removed: mtl

Dependency ranges changed: apiary, apiary-cookie

API changes (from Hackage documentation)

- Web.Apiary.ClientSession: setRawSession :: (MonadIO m, HasSession) => Maybe DiffTime -> SetCookie -> ActionT m ()
- Web.Apiary.ClientSession: setSessionWith :: (MonadIO m, HasSession) => (SetCookie -> SetCookie) -> ByteString -> ByteString -> ActionT m ()
+ Web.Apiary.ClientSession: angularXsrfCookieName :: SessionConfig -> Maybe ByteString
+ Web.Apiary.ClientSession: checkToken :: (Functor n, MonadIO n, HasSession) => ApiaryT c n m a -> ApiaryT c n m a
+ Web.Apiary.ClientSession: csrfToken :: (MonadIO m, HasSession) => ActionT 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.Explicit: SessionConfig :: FilePath -> DiffTime -> Maybe ByteString -> Maybe ByteString -> Bool -> Bool -> Maybe ByteString -> ByteString -> Either HeaderName ByteString -> Int -> SessionConfig
+ Web.Apiary.ClientSession.Explicit: angularXsrfCookieName :: SessionConfig -> Maybe ByteString
+ Web.Apiary.ClientSession.Explicit: checkToken :: (Functor n, MonadIO n) => Session -> ApiaryT c n m a -> ApiaryT c n m a
+ Web.Apiary.ClientSession.Explicit: csrfToken :: MonadIO m => Session -> ActionT m ByteString
+ Web.Apiary.ClientSession.Explicit: csrfTokenCheckingName :: SessionConfig -> Either HeaderName ByteString
+ Web.Apiary.ClientSession.Explicit: csrfTokenCookieName :: SessionConfig -> ByteString
+ Web.Apiary.ClientSession.Explicit: csrfTokenLength :: SessionConfig -> Int
+ Web.Apiary.ClientSession.Explicit: data Session
+ Web.Apiary.ClientSession.Explicit: data SessionConfig
+ Web.Apiary.ClientSession.Explicit: domain :: SessionConfig -> Maybe ByteString
+ Web.Apiary.ClientSession.Explicit: getSessionValue :: Session -> UTCTime -> ByteString -> Maybe ByteString
+ Web.Apiary.ClientSession.Explicit: httpOnly :: SessionConfig -> Bool
+ Web.Apiary.ClientSession.Explicit: keyFile :: SessionConfig -> FilePath
+ Web.Apiary.ClientSession.Explicit: maxAge :: SessionConfig -> DiffTime
+ Web.Apiary.ClientSession.Explicit: mkSessionCookie :: SessionConfig -> Key -> ByteString -> ByteString -> IO SetCookie
+ Web.Apiary.ClientSession.Explicit: path :: SessionConfig -> Maybe ByteString
+ Web.Apiary.ClientSession.Explicit: secure :: SessionConfig -> Bool
+ Web.Apiary.ClientSession.Explicit: session :: (Functor n, MonadIO n, Strategy w, Query a) => Session -> ByteString -> Proxy (w a) -> ApiaryT (SNext w as a) n m b -> ApiaryT as n m b
+ Web.Apiary.ClientSession.Explicit: setSession :: MonadIO m => Session -> ByteString -> ByteString -> ActionT m ()
+ Web.Apiary.ClientSession.Explicit: withSession :: MonadIO m => SessionConfig -> (Session -> m b) -> m b
- Web.Apiary.ClientSession: SessionConfig :: FilePath -> Maybe DiffTime -> Maybe ByteString -> Maybe ByteString -> Bool -> Bool -> SessionConfig
+ Web.Apiary.ClientSession: SessionConfig :: FilePath -> DiffTime -> Maybe ByteString -> Maybe ByteString -> Bool -> Bool -> Maybe ByteString -> ByteString -> Either HeaderName ByteString -> Int -> SessionConfig
- Web.Apiary.ClientSession: maxAge :: SessionConfig -> Maybe DiffTime
+ Web.Apiary.ClientSession: maxAge :: SessionConfig -> DiffTime
- Web.Apiary.ClientSession: session :: (Strategy w, Query a, HasSession, Monad n, Functor n) => ByteString -> Proxy (w a) -> ApiaryT (SNext w as a) n m b -> ApiaryT as n m b
+ Web.Apiary.ClientSession: session :: (Functor n, MonadIO n, Strategy w, Query a, HasSession) => ByteString -> Proxy (w a) -> ApiaryT (SNext w as a) n m b -> ApiaryT as n m b

Files

apiary-clientsession.cabal view
@@ -1,5 +1,5 @@ name:                apiary-clientsession-version:             0.8.0.0+version:             0.9.0.0 synopsis:            clientsession support for apiary web framework. description:   example: <https://github.com/philopon/apiary/blob/master/examples/auth.hs>@@ -18,18 +18,23 @@  library   exposed-modules:     Web.Apiary.ClientSession+                       Web.Apiary.ClientSession.Explicit   other-modules:       Web.Apiary.ClientSession.Internal   other-extensions:       build-depends:       base               >=4.6   && <4.8-                     , mtl                >=2.1   && <2.3                      , bytestring         >=0.10  && <0.11-                     , apiary             >=0.8   && <0.9-                     , apiary-cookie      >=0.8   && <0.9+                     , apiary             >=0.9   && <0.10+                     , apiary-cookie      >=0.9   && <0.10                      , clientsession      >=0.9   && <0.10                      , tagged             >=0.7   && <0.8                      , time               >=1.4   && <1.5                      , data-default-class >=0.0   && <0.1                      , reflection         >=1.4   && <1.5+                     , binary             >=0.7   && <0.8+                     , crypto-random      >=0.0   && <0.1+                     , cprng-aes          >=0.5   && <0.6+                     , base64-bytestring  >=1.0   && <1.1+                     , http-types         >=0.8   && <0.9    hs-source-dirs:      src   ghc-options:         -O2 -Wall
src/Web/Apiary/ClientSession.hs view
@@ -1,19 +1,56 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE FlexibleContexts #-}+ module Web.Apiary.ClientSession     ( HasSession-    , SessionConfig(..)+    , I.SessionConfig(..)     , withSession     -- * setter     , setSession-    , setSessionWith-    , setRawSession+    , csrfToken     -- * filter     , session+    , checkToken     -- * Reexport-    -- | def     , module Data.Default.Class+    -- | deleteCookie     , module Web.Apiary.Cookie     ) where +import Web.Apiary+ import Data.Default.Class-import Web.Apiary.ClientSession.Internal-import Web.Apiary.Cookie+import qualified Web.Apiary.ClientSession.Internal as I+import Web.Apiary.Cookie (deleteCookie)+import Data.Reflection+import qualified Data.ByteString as S+import Control.Monad.Apiary.Filter.Internal.Strategy+import Data.Proxy++type HasSession = Given I.Session++withSession :: MonadIO m => I.SessionConfig -> (HasSession => m b) -> m b+withSession c m = I.withSession c (\s -> give s m)++setSession :: (MonadIO m, HasSession)+           => S.ByteString -> S.ByteString -> ActionT m ()+setSession = I.setSession given++-- | 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 :: (MonadIO m, HasSession) => ActionT m S.ByteString+csrfToken = I.csrfToken given++session :: (Functor n, MonadIO n, Strategy w, Query a, HasSession)+        => S.ByteString -> Proxy (w a)+        -> ApiaryT (SNext w as a) n m b -> ApiaryT as n m b+session = I.session given++-- | check csrf token. since 0.9.0.0.+checkToken :: (Functor n, MonadIO n, HasSession)+           => ApiaryT c n m a -> ApiaryT c n m a+checkToken = I.checkToken given
+ src/Web/Apiary/ClientSession/Explicit.hs view
@@ -0,0 +1,20 @@+module Web.Apiary.ClientSession.Explicit+    (+      Session, SessionConfig(..)+    , withSession+    -- * actions+    , setSession+    , csrfToken+    -- * filters+    , session, checkToken+    -- * lowlevels+    , mkSessionCookie, getSessionValue+    -- * reexports+    , module Data.Default.Class+    -- | deleteCookie+    , module Web.Apiary.Cookie+    ) where++import Web.Apiary.ClientSession.Internal+import Data.Default.Class+import Web.Apiary.Cookie(deleteCookie)
src/Web/Apiary/ClientSession/Internal.hs view
@@ -5,95 +5,155 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE PackageImports #-}  module Web.Apiary.ClientSession.Internal where -import Control.Monad.Trans+import Control.Monad+import Control.Applicative+import Control.Arrow +import Web.Apiary.Wai+import qualified Network.HTTP.Types as HTTP+ import Web.Apiary hiding (Default(..)) import Web.Apiary.Cookie import Web.Apiary.Cookie.Internal import Web.ClientSession  import Data.Proxy+import Data.Maybe import Data.Time import Data.Default.Class-import Data.Reflection+import Data.Binary+import Data.IORef+import qualified Data.ByteString.Base64 as Base64 +import "crypto-random" Crypto.Random+import Crypto.Random.AESCtr+ import Control.Monad.Apiary.Filter.Internal import Control.Monad.Apiary.Filter.Internal.Strategy  import qualified Data.ByteString as S+import qualified Data.ByteString.Lazy as L + data Session = Session     { key       :: Key+    , tokenGen  :: IORef AESRNG     , config    :: SessionConfig     }  data SessionConfig = SessionConfig-    { keyFile  :: FilePath-    , maxAge   :: Maybe DiffTime-    , path     :: Maybe S.ByteString-    , domain   :: Maybe S.ByteString-    , httpOnly :: Bool-    , secure   :: Bool+    { keyFile               :: FilePath+    , maxAge                :: DiffTime+    , path                  :: Maybe S.ByteString+    , domain                :: Maybe S.ByteString+    , httpOnly              :: Bool+    , secure                :: Bool++    , angularXsrfCookieName :: Maybe S.ByteString+    , csrfTokenCookieName   :: S.ByteString++    , csrfTokenCheckingName :: Either HTTP.HeaderName S.ByteString+    , csrfTokenLength       :: Int     }  instance Default SessionConfig where     def = SessionConfig-        defaultKeyFile (Just (24 * 60 * 60)) Nothing Nothing True True--type HasSession = Given Session--cond :: (a -> Bool) -> (a -> b) -> (a -> b) -> a -> b-cond p t f a = if p a then t a else f a+        defaultKeyFile (24 * 60 * 60) Nothing Nothing True True+        Nothing "_token" (Right "_token") 40 -withSession :: MonadIO m => SessionConfig -> (HasSession => m b) -> m b+withSession :: MonadIO m => SessionConfig -> (Session -> m b) -> m b withSession cfg@SessionConfig{..} m = do     k <- liftIO $ getKey keyFile-    let sess = Session k cfg-    give sess m+    p <- liftIO $ makeSystem >>= newIORef+    let sess = Session k p cfg+    m sess -setMaxAge :: SetCookie -> Maybe DiffTime -> IO SetCookie-setMaxAge s (Just a) = do+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 key k v = do     t <- getCurrentTime-    return s { setCookieExpires = Just $ addUTCTime (realToFrac a) t-             , setCookieMaxAge  = Just a-             }-setMaxAge s Nothing = return s+    let expire = addUTCTime (realToFrac $ maxAge conf) t+    v' <- encryptIO key $ L.toStrict $ encode (BinUTCTime expire, v)+    return def { setCookieName     = k+               , setCookieValue    = v'+               , setCookiePath     = path conf+               , setCookieExpires  = Just expire+               , setCookieMaxAge   = Just (maxAge conf)+               , setCookieDomain   = domain conf+               , setCookieHttpOnly = httpOnly conf+               , setCookieSecure   = secure conf+               } -encryptValue :: HasSession => SetCookie -> IO SetCookie-encryptValue s = do-    v' <- encryptIO (key given) (setCookieValue s)-    return $ s { setCookieValue = v' }-    -setRawSession :: (MonadIO m, HasSession) => Maybe DiffTime -> SetCookie -> ActionT m ()-setRawSession age s = do-    s' <- liftIO $ encryptValue =<< setMaxAge s age-    setCookie s'+getSessionValue :: Session -> UTCTime -- ^ current time+                -> S.ByteString +                -> Maybe S.ByteString+getSessionValue Session{key = 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 -setSessionWith :: (MonadIO m, HasSession)-               => (SetCookie -> SetCookie) -- ^ postprocess-               -> S.ByteString -- ^ key-               -> S.ByteString -- ^ value-               -> ActionT m ()-setSessionWith f k v = do-    let Session{..} = given-    setRawSession (maxAge config) $ f def-        { setCookieName     = k -        , setCookieValue    = v-        , setCookiePath     = path config-        , setCookieDomain   = domain config-        , setCookieHttpOnly = httpOnly config-        , setCookieSecure   = secure config-        }+setSession :: MonadIO m => Session -> S.ByteString -> S.ByteString -> ActionT m ()+setSession sess k v = do+    s <- liftIO $ mkSessionCookie (config sess) (key sess) k v+    setCookie s -setSession :: (MonadIO m, HasSession)-           => S.ByteString -- ^ key-           -> S.ByteString -- ^ value-           -> ActionT m ()-setSession = setSessionWith id+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) -session :: (Strategy w, Query a, HasSession, Monad n, Functor n)-        => S.ByteString -> Proxy (w a) -> ApiaryT (SNext w as a) n m b -> ApiaryT as n m b-session ky p = function $ \l r -> readStrategy readQuery ((ky ==) . fst) p-    (map (\(k,b) -> (k, decrypt (key given) b)) $ cookie' r) l+csrfToken :: MonadIO m => Session -> ActionT m S.ByteString+csrfToken Session{..} = do+    tok <- liftIO $ newToken (csrfTokenLength config) tokenGen +    sc <- liftIO $ mkSessionCookie config key (csrfTokenCookieName config) tok+    setCookie sc+    maybe (return ()) (setCookie . ngCookie sc tok) (angularXsrfCookieName config)+    return tok+  where+    ngCookie sc tok k = sc { setCookieName     = k+                           , setCookieValue    = tok+                           , setCookieHttpOnly = False+                           }++session :: (Functor n, MonadIO n, Strategy w, Query a) => Session+        -> S.ByteString -> Proxy (w a) -> ApiaryT (SNext w as a) n m b -> ApiaryT as n m b+session sess k p = focus $ \l -> do+    r   <- getRequest+    t   <- liftIO getCurrentTime+    let mbr = readStrategy readQuery ((k ==) . fst) p+            (map (second $ getSessionValue sess t) $ cookie' r) l+    maybe empty return mbr++checkToken :: (Functor n, MonadIO n)+           => Session+           -> ApiaryT c n m a+           -> ApiaryT c n m a+checkToken sess@Session{..} = focus $ \l -> do+    r <- getRequest+    p <- getReqParams++    t <- liftIO getCurrentTime+    let stok = getSessionValue sess t =<< +               lookup (csrfTokenCookieName config) (cookie' r)+    guard (isJust stok)+    +    qtok <- return . join $ case csrfTokenCheckingName config of+        Right name -> lookup name $ reqParams pByteString r p []+        Left name  -> lookup name $ map (\(k,v) -> (k, Just v)) $ requestHeaders r+    guard (isJust qtok)++    if qtok == stok then return l else empty+