Hello community,

here is the log from the commit of package ghc-servant-auth-cookie for 
openSUSE:Factory checked in at 2017-08-31 20:59:08
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/ghc-servant-auth-cookie (Old)
 and      /work/SRC/openSUSE:Factory/.ghc-servant-auth-cookie.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "ghc-servant-auth-cookie"

Thu Aug 31 20:59:08 2017 rev:3 rq:513483 version:0.5.0.5

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/ghc-servant-auth-cookie/ghc-servant-auth-cookie.changes
  2017-05-18 20:51:03.384090972 +0200
+++ 
/work/SRC/openSUSE:Factory/.ghc-servant-auth-cookie.new/ghc-servant-auth-cookie.changes
     2017-08-31 20:59:11.188461327 +0200
@@ -1,0 +2,5 @@
+Thu Jul 27 14:08:14 UTC 2017 - [email protected]
+
+- Update to version 0.5.0.5.
+
+-------------------------------------------------------------------

Old:
----
  servant-auth-cookie-0.4.4.tar.gz

New:
----
  servant-auth-cookie-0.5.0.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ ghc-servant-auth-cookie.spec ++++++
--- /var/tmp/diff_new_pack.ZmsgKF/_old  2017-08-31 20:59:12.300305110 +0200
+++ /var/tmp/diff_new_pack.ZmsgKF/_new  2017-08-31 20:59:12.316302863 +0200
@@ -19,7 +19,7 @@
 %global pkg_name servant-auth-cookie
 %bcond_with tests
 Name:           ghc-%{pkg_name}
-Version:        0.4.4
+Version:        0.5.0.5
 Release:        0
 Summary:        Authentication via encrypted cookies
 License:        BSD-3-Clause

++++++ servant-auth-cookie-0.4.4.tar.gz -> servant-auth-cookie-0.5.0.5.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/servant-auth-cookie-0.4.4/CHANGELOG.md 
new/servant-auth-cookie-0.5.0.5/CHANGELOG.md
--- old/servant-auth-cookie-0.4.4/CHANGELOG.md  2017-04-15 11:57:55.000000000 
+0200
+++ new/servant-auth-cookie-0.5.0.5/CHANGELOG.md        2017-07-13 
10:40:23.000000000 +0200
@@ -1,6 +1,40 @@
 # Change Log
 
-## [0.4.4]
+## [HEAD]
+
+## [0.5.0.5] - 2017-07-13
+### Changed
+- Fixed dependencies' bounds.
+
+## [0.5.0.4] - 2017-05-27
+### Changed
+- Fixed dependencies' bounds.
+
+## [0.5.0.3] - 2017-05-24
+### Changed
+- Fixed dependencies' bounds.
+
+## [0.5.0.2] - 2017-04-26
+### Changed
+- Fixed dependencies' bounds.
+
+## [0.5.0.1] - 2017-04-16
+### Changed
+- Fixed incompatibility with older versions of GHC.
+
+## [0.5.0] - 2017-04-15
+### Changed
+- Server keys management:
+  - `ServerKey` becomes `ServerKeySet`.
+  - `mkServerKeyFromBytes` becomes `mkPersistentServerKey`.
+
+### Deleted
+- `mkServerKey` (instead use custom instance of `ServerKeySet`.
+
+### Added
+- class `Cookied` and function `cookied` to faciliate usage of mutable server 
keys.
+
+## [0.4.4] - 2017-04-15
 ### Added
 - Tests for the example.
 - `parseSessionRequest` and `parseSessionResponse` functions.
@@ -87,7 +121,13 @@
 - Initial version of the package.
 
 
-[HEAD]:    ../../compare/v0.4.4...HEAD
+[HEAD]:    ../../compare/v0.5.0.5...HEAD
+[0.5.0.5]: ../../compare/v0.5.0.4...v0.5.0.5
+[0.5.0.4]: ../../compare/v0.5.0.3...v0.5.0.4
+[0.5.0.3]: ../../compare/v0.5.0.2...v0.5.0.3
+[0.5.0.2]: ../../compare/v0.5.0.1...v0.5.0.2
+[0.5.0.1]: ../../compare/v0.5.0...v0.5.0.1
+[0.5.0]:   ../../compare/v0.4.4...v0.5.0
 [0.4.4]:   ../../compare/v0.4.3.3...v0.4.4
 [0.4.3.3]: ../../compare/v0.4.3.2...v0.4.3.3
 [0.4.3.2]: ../../compare/v0.4.3.1...v0.4.3.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/servant-auth-cookie-0.4.4/example/Main.hs 
new/servant-auth-cookie-0.5.0.5/example/Main.hs
--- old/servant-auth-cookie-0.4.4/example/Main.hs       2017-04-15 
11:57:55.000000000 +0200
+++ new/servant-auth-cookie-0.5.0.5/example/Main.hs     2017-07-13 
10:40:23.000000000 +0200
@@ -1,11 +1,6 @@
-{-# LANGUAGE CPP                   #-}
-{-# LANGUAGE DataKinds             #-}
-{-# LANGUAGE DeriveGeneric         #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE OverloadedStrings     #-}
-{-# LANGUAGE RecordWildCards       #-}
-{-# LANGUAGE TypeFamilies          #-}
-{-# LANGUAGE TypeOperators         #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module Main (main) where
 import AuthAPI (app, authSettings)
@@ -15,13 +10,30 @@
 import Network.Wai.Handler.Warp (run)
 import Servant.Server.Experimental.Auth.Cookie
 
+#if MIN_VERSION_servant (0,9,1) && MIN_VERSION_directory (1,2,5)
+import FileKeySet (mkFileKeySet, FileKSParams(..), mkFileKey)
+#endif
+
+-- To use mutable server keys we need servant-9.1 and
+-- directory-1.2.5 (or higher). Otherwise the only (sane) choice is a
+-- persistent key.
+
 main :: IO ()
 main = do
   rs <- mkRandomSource drgNew 1000
-  -- NOTE:
-  -- Every time the application is executed, a new server key is
-  -- created. This means, once you restart the app, already existing
-  -- cookies will be invalidated.
-  sk <- mkServerKey 16 Nothing
-  run 8080 (app authSettings rs sk)
 
+#if MIN_VERSION_servant (0,9,1) && MIN_VERSION_directory (1,2,5)
+  let fksp = FileKSParams
+        { fkspKeySize = 16
+        , fkspMaxKeys = 3
+        , fkspPath = "./test-key-set"
+        }
+
+  k <- mkFileKeySet fksp
+  let generateKey = mkFileKey fksp
+#else
+  let k = mkPersistentServerKey "0123456789abcdef"
+  let generateKey = return ()
+#endif
+
+  run 8080 (app authSettings generateKey rs k)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/servant-auth-cookie-0.4.4/example/Test.hs 
new/servant-auth-cookie-0.5.0.5/example/Test.hs
--- old/servant-auth-cookie-0.4.4/example/Test.hs       2017-04-15 
11:57:55.000000000 +0200
+++ new/servant-auth-cookie-0.5.0.5/example/Test.hs     2017-07-13 
10:40:23.000000000 +0200
@@ -1,6 +1,8 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE CPP #-}
 {-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE TupleSections #-}
 
 import Prelude ()
 import Prelude.Compat
@@ -9,18 +11,20 @@
 import Data.Time.Clock (UTCTime(..))
 import Control.Monad.IO.Class (liftIO)
 import AuthAPI (app, authSettings, LoginForm(..), homePage, loginPage, 
Account(..))
-import Test.Hspec (Spec, hspec, describe, it)
+import Test.Hspec (Spec, hspec, describe, context, it)
 import Test.Hspec.Wai (WaiSession, WaiExpectation, shouldRespondWith, with, 
request, get)
 import Text.Blaze.Renderer.Utf8 (renderMarkup)
+import Text.Blaze (Markup)
 import Servant (Proxy(..))
 import Crypto.Random (drgNew)
 import Servant (FormUrlEncoded, contentType)
 import Servant.Server.Experimental.Auth.Cookie
-import Network.HTTP.Types (methodGet, methodPost, hContentType, hCookie)
+import Network.HTTP.Types (Header, methodGet, methodPost, hContentType, 
hCookie)
 import Network.HTTP.Media.RenderHeader (renderHeader)
 import Network.Wai.Test (SResponse(..))
+import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as BSL
-import qualified Data.ByteString.Lazy.Char8 as BSC8
+import qualified Data.ByteString.Lazy.Char8 as BSLC8
 
 #if MIN_VERSION_hspec_wai (0,7,0)
 import Test.Hspec.Wai.Matcher (bodyEquals, ResponseMatcher(..), MatchBody(..))
@@ -34,142 +38,173 @@
 import Servant (ToFormUrlEncoded, mimeRender)
 #endif
 
+#if MIN_VERSION_servant (0,9,1) && MIN_VERSION_directory (1,2,5)
+import FileKeySet (mkFileKeySet, FileKSParams(..), mkFileKey)
+import Control.Arrow ((***))
+import Control.Monad (void, when)
+import Data.Monoid ((<>))
+import Control.Exception.Base (bracket)
+import Network.HTTP.Types (urlEncode)
+import Test.Hspec (shouldBe, shouldSatisfy)
+import System.Directory (removeDirectoryRecursive, doesDirectoryExist)
+import qualified Data.ByteString.Char8 as BSC8
+import qualified Text.Blaze.Html5 as H
+import qualified Text.Blaze.Html5.Attributes as A
+#endif
+
+
+data SpecState where
+  SpecState :: (ServerKeySet k) =>
+    { ssRandomSource :: RandomSource
+    , ssAuthSettings :: AuthCookieSettings
+    , ssServerKeySet :: k
+    , ssGenerateKey  :: IO ()
+    } -> SpecState
 
-data SpecState = SpecState {
-    ssRandomSource :: RandomSource
-  , ssServerKey    :: ServerKey
-  , ssAuthSettings :: AuthCookieSettings
-  }
 
 main :: IO ()
-main = withState (hspec . spec) where
-  withState f = do
-    let ssAuthSettings = authSettings
-    ssRandomSource <- mkRandomSource drgNew 1000
-    ssServerKey <- mkServerKey 16 Nothing
-    f $ SpecState {..}
-
-
-spec :: SpecState -> Spec
-spec SpecState {..} = with (return $ app ssAuthSettings ssRandomSource 
ssServerKey) $ do
-
-  let formContentType = (
-          hContentType
-        , renderHeader $ contentType (Proxy :: Proxy FormUrlEncoded))
+main = do
+  rs <- mkRandomSource drgNew 1000
+
+  return SpecState
+    { ssRandomSource = rs
+    , ssAuthSettings = authSettings
+    , ssServerKeySet = mkPersistentServerKey "0123456789abcdef"
+    , ssGenerateKey  = return ()
+    } >>= hspec . basicSpec
+
+#if MIN_VERSION_servant (0,9,1) && MIN_VERSION_directory (1,2,5)
+  let rmDir name = doesDirectoryExist name
+        >>= \exists -> when exists $ removeDirectoryRecursive name
+
+  bracket
+    (do
+       let keySetDir = "./test-key-set"
+       rmDir keySetDir
+       return FileKSParams
+         { fkspMaxKeys = 3
+         , fkspKeySize = 16
+         , fkspPath = keySetDir
+         } >>= \fksp -> (fksp,) <$> mkFileKeySet fksp)
+
+    (rmDir . fkspPath . fst)
+
+    (\(fksp, ks) -> hspec . renewalSpec $ SpecState
+         { ssRandomSource = rs
+         , ssAuthSettings = authSettings
+         , ssServerKeySet = ks
+         , ssGenerateKey  = mkFileKey fksp
+         })
+#endif
 
-  describe "home page" $ do
+basicSpec :: SpecState -> Spec
+basicSpec ss@(SpecState {..}) = describe "basic functionality" $ with
+  (return $ app ssAuthSettings ssGenerateKey ssRandomSource ssServerKeySet) $ 
do
+
+  context "home page" $ do
     it "responds successfully" $ do
-      get "/" `shouldRespondWith` 200 {
-        matchBody = matchBody' $ renderMarkup homePage
-        }
+      get "/" `shouldRespondWithMarkup` homePage
 
-  describe "login page" $ do
+  context "login page" $ do
     it "responds successfully" $ do
-      get "/login" `shouldRespondWith` 200 {
-        matchBody = matchBody' $ renderMarkup (loginPage True)
-        }
+      get "/login" `shouldRespondWithMarkup` (loginPage True)
 
     it "shows message on incorrect login" $ do
-      let loginForm = encode $ LoginForm {
-            lfUsername = "noname"
-          , lfPassword = "noname"
-          }
-      let r = request methodPost "/login" [formContentType] loginForm
-      r `shouldRespondWith` 200 {
-        matchBody = matchBody' $ renderMarkup (loginPage False)
-        }
-
-  describe "private page" $ do
-    let loginForm = encode $ LoginForm {
-          lfUsername = "mr_foo"
-        , lfPassword = "password1"
-        }
-    let loginRequest = request methodPost "/login" [formContentType] loginForm
+      login "noname" "noname" `shouldRespondWithMarkup` (loginPage False)
+
+  context "private page" $ do
+    let loginRequest = login "mr_foo" "password1"
 
     it "rejects requests without cookies" $ do
-      let r = get "/private"
-      r `shouldRespondWith` 403 { matchBody = matchBody' "No cookies" }
+      get "/private" `shouldRespondWith` 403 { matchBody = matchBody' "No 
cookies" }
 
     it "accepts requests with proper cookies" $ do
       (SResponse {..}) <- loginRequest
       let cookieValue = fromMaybe
             (error "cookies aren't available")
             (lookup "set-cookie" simpleHeaders)
-
-      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
-      r `shouldRespondWith` 200
+      getPrivate cookieValue `shouldRespondWith` 200
 
     it "accepts requests with proper cookies (sanity check)" $ do
-      (SResponse {..}) <- loginRequest
-
-      cookieValue <- liftIO $ do
-        session <- maybe
-          (error "cookies aren't available")
-          (decryptSession ssAuthSettings ssServerKey)
-          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account
-
-        renderSession ssAuthSettings ssRandomSource ssServerKey session
-
-      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
-      r `shouldRespondWith` 200
-
+      cookieValue <- loginRequest
+        >>= liftIO . forgeCookies ss authSettings ssServerKeySet
+      getPrivate cookieValue `shouldRespondWith` 200
 
     it "rejects requests with incorrect MAC" $ do
-      (SResponse {..}) <- loginRequest
-
-      cookieValue <- liftIO $ do
-        session <- maybe
-          (error "cookies aren't available")
-          (decryptSession ssAuthSettings ssServerKey)
-          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account
-
-        sk <- mkServerKey 16 Nothing
-        renderSession ssAuthSettings ssRandomSource sk session
+      let newServerKeySet = mkPersistentServerKey "0000000000000000"
+      cookieValue <- loginRequest
+        >>= liftIO . forgeCookies ss authSettings newServerKeySet
+      getPrivate cookieValue `shouldRespondWithException` (IncorrectMAC "")
 
-      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
-
-      r `shouldRespondWithException` (IncorrectMAC "")
+    it "rejects requests with malformed expiration time" $ do
+      let newAuthSettings = authSettings { acsExpirationFormat = "%0Y%m%d" }
+      cookieValue <- loginRequest
+        >>= liftIO . forgeCookies ss newAuthSettings ssServerKeySet
+      getPrivate cookieValue `shouldRespondWithException` 
(CannotParseExpirationTime "")
 
+    it "rejects requests with expired cookies" $ do
+      let newAuthSettings = authSettings { acsMaxAge = 0 }
+      cookieValue <- loginRequest
+        >>= liftIO . forgeCookies ss newAuthSettings ssServerKeySet
+      let t = UTCTime (toEnum 0) 0
+      getPrivate cookieValue `shouldRespondWithException` (CookieExpired t t)
+
+
+#if MIN_VERSION_servant (0,9,1) && MIN_VERSION_directory (1,2,5)
+renewalSpec :: SpecState -> Spec
+renewalSpec (SpecState {..}) = describe "renewal functionality" $ with
+  (return $ app ssAuthSettings ssGenerateKey ssRandomSource ssServerKeySet) $ 
do
+
+  context "keys" $ do
+    it "automatically creates a key" $ do
+      keys <- extractKeys
+      liftIO $ keys `shouldSatisfy` ((== 1) . length)
+
+    it "adds new key" $ do
+      keys <- extractKeys
+      addKey
+      keys' <- extractKeys
+      liftIO $ keys `shouldBe` (tail keys')
+
+    it "removes a key" $ do
+      keys <- extractKeys
+      remKey $ last keys
+      keys' <- extractKeys
+      liftIO $ keys' `shouldBe` (init keys)
 
-    it "rejects requests with malformed expiration time" $ do
-      (SResponse {..}) <- loginRequest
+  context "cookies" $ do
+    let loginRequest = login "mr_foo" "password1"
 
-      cookieValue <- liftIO $ do
-        session <- maybe
+    let getCookieValue req = req >>= \resp -> return $ fromMaybe
           (error "cookies aren't available")
-          (decryptSession ssAuthSettings ssServerKey)
-          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account
+          (lookup "set-cookie" $ simpleHeaders resp)
 
-        renderSession
-          ssAuthSettings { acsExpirationFormat = "%0Y%m%d" }
-          ssRandomSource
-          ssServerKey
-          session
+    it "rejects requests with deleted keys" $ do
+      cookieValue <- getCookieValue loginRequest
+      getPrivate cookieValue `shouldRespondWith` 200
 
-      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
-      r `shouldRespondWithException` (CannotParseExpirationTime "")
+      key <- head <$> extractKeys
+      addKey >> remKey key
 
+      getPrivate cookieValue `shouldRespondWith` 403
 
-    it "rejects requests with expired cookies" $ do
-      (SResponse {..}) <- loginRequest
-
-      cookieValue <- liftIO $ do
-        session <- maybe
-          (error "cookies aren't available")
-          (decryptSession ssAuthSettings ssServerKey)
-          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account
+    it "accepts requests with old key and renews cookie" $ do
+      cookieValue <- getCookieValue loginRequest
+      getPrivate cookieValue `shouldRespondWith` 200
 
-        renderSession
-          ssAuthSettings { acsMaxAge = 0 }
-          ssRandomSource
-          ssServerKey
-          session
+      key <- head <$> extractKeys
+      addKey
+      newCookieValue <- getCookieValue (getPrivate cookieValue)
 
-      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
-      let dummyTime = UTCTime (toEnum 0) 0
-
-      r `shouldRespondWithException` (CookieExpired dummyTime dummyTime)
+      remKey key
+      getPrivate newCookieValue `shouldRespondWith` 200
 
+    it "does not renew cookies for the newest key" $ do
+      cookieValue <- getCookieValue loginRequest
+      _ <- getPrivate cookieValue `shouldRespondWith` 200
+      r <- getPrivate cookieValue
+      liftIO $ (lookup "set-cookie" $ simpleHeaders r) `shouldBe` Nothing
+#endif
 
 #if MIN_VERSION_hspec_wai (0,7,0)
 matchBody' :: BSL.ByteString -> MatchBody
@@ -192,8 +227,69 @@
 
 shouldRespondWithException :: WaiSession SResponse -> AuthCookieException -> 
WaiExpectation
 shouldRespondWithException req ex = do
-  let exception = BSC8.pack . head . words . show $ ex
-  (shrinkBody (BSC8.length exception) <$> req) `shouldRespondWith` 403 {
-    matchBody = matchBody' exception
+  let exception = BSLC8.pack . head . words . show $ ex
+  (shrinkBody (BSLC8.length exception) <$> req) `shouldRespondWith` 403 {
+      matchBody = matchBody' exception
+    }
+
+shouldRespondWithMarkup :: WaiSession SResponse -> Markup -> WaiExpectation
+shouldRespondWithMarkup req markup = do
+  req `shouldRespondWith` 200 {
+      matchBody = matchBody' $ renderMarkup markup
     }
 
+formContentType :: Header
+formContentType = (
+    hContentType
+  , renderHeader $ contentType (Proxy :: Proxy FormUrlEncoded))
+
+login :: String -> String -> WaiSession SResponse
+login lfUsername lfPassword = request
+  methodPost "/login" [formContentType] (encode LoginForm {..})
+
+getPrivate :: BS.ByteString -> WaiSession SResponse
+getPrivate cookieValue = request
+  methodGet "/private" [(hCookie, cookieValue)] ""
+
+extractSession :: SpecState -> SResponse -> IO (WithMetadata Account)
+extractSession SpecState {..} SResponse {..} = maybe
+  (error "cookies aren't available")
+  (decryptSession ssAuthSettings ssServerKeySet)
+  (parseSessionResponse ssAuthSettings simpleHeaders)
+
+forgeCookies :: (ServerKeySet k)
+  => SpecState
+  -> AuthCookieSettings
+  -> k
+  -> SResponse
+  -> IO BS.ByteString
+forgeCookies ss newAuthSettings newServerKeySet r = extractSession ss r
+  >>= renderSession newAuthSettings (ssRandomSource ss) newServerKeySet . 
wmData
+
+
+#if MIN_VERSION_servant (0,9,1) && MIN_VERSION_directory (1,2,5)
+extractKeys :: WaiSession [BS.ByteString]
+extractKeys = (extractKeys' . BSL.toStrict . simpleBody) <$> get "/keys" where
+  del = '#'
+
+  (openTag, closeTag) = (id *** BS.drop 1) $ BSC8.span (/= del) $
+    BSL.toStrict . renderMarkup $
+      H.span H.! A.class_ "key" $ H.toHtml [del]
+
+  shrinkBy prefix = BS.drop . BS.length $ prefix
+
+  extractKeys' body = let
+    body' = snd $ BS.breakSubstring openTag body
+    (key, rest) = shrinkBy openTag *** shrinkBy closeTag $
+       BS.breakSubstring closeTag body'
+    in if BS.null body'
+       then []
+       else key:(extractKeys' rest)
+
+addKey :: WaiSession ()
+addKey = void $ get "/keys/add"
+
+remKey :: BS.ByteString -> WaiSession ()
+remKey key = void $ get $ "/keys/rem/" <> (urlEncode True $ key)
+#endif
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/servant-auth-cookie-0.4.4/servant-auth-cookie.cabal 
new/servant-auth-cookie-0.5.0.5/servant-auth-cookie.cabal
--- old/servant-auth-cookie-0.4.4/servant-auth-cookie.cabal     2017-04-15 
11:57:55.000000000 +0200
+++ new/servant-auth-cookie-0.5.0.5/servant-auth-cookie.cabal   2017-07-13 
10:40:23.000000000 +0200
@@ -1,5 +1,5 @@
 name:                servant-auth-cookie
-version:             0.4.4
+version:             0.5.0.5
 synopsis:            Authentication via encrypted cookies
 description:         Authentication via encrypted client-side cookies,
                      inspired by client-session library by Michael Snoyman and 
based on
@@ -26,12 +26,18 @@
   default:            False
 
 flag servant9
-  description:        Use servant-0.9
+  description:        Use servant-0.9 (or higher)
+  manual:             False
+  default:            True
+
+flag servant91
+  description:        Use servant-0.9.1 (or higher)
   manual:             False
   default:            True
 
 flag build-examples
   description:        Build example executables.
+  manual:             True
   default:            False
 
 
@@ -45,14 +51,14 @@
                , bytestring
                , cereal         >= 0.5   && < 0.6
                , cookie         >= 0.4.1 && < 0.5
-               , cryptonite     >= 0.14  && < 0.23
+               , cryptonite     >= 0.14  && < 0.25
                , data-default
                , exceptions     >= 0.8   && < 0.9
                , http-types     >= 0.9   && < 0.10
                , memory         >= 0.11  && < 0.15
                , mtl            >= 2.0  && < 3.0
-               , servant        >= 0.5   && < 0.11
-               , servant-server >= 0.5   && < 0.11
+               , servant        >= 0.5   && < 0.12
+               , servant-server >= 0.5   && < 0.12
                , tagged         == 0.8.*
                , time           >= 1.5   && < 1.8.1
                , transformers   >= 0.4   && < 0.6
@@ -70,9 +76,14 @@
       servant >= 0.9,
       http-api-data == 0.3.*
   else
-    build-depends:
-      servant < 0.9,
-      bytestring-conversion >= 0.3.1 && <0.4
+    if flag(servant91)
+      build-depends:
+        servant >= 0.9,
+        http-api-data == 0.3.*
+    else
+      build-depends:
+        servant < 0.9,
+        bytestring-conversion >= 0.3.1 && <0.4
 
 test-suite tests
   type:           exitcode-stdio-1.0
@@ -87,12 +98,13 @@
                , QuickCheck     >= 2.4  && < 3.0
                , bytestring
                , cereal         >= 0.5  && < 0.6
-               , cryptonite     >= 0.14 && < 0.23
+               , cryptonite     >= 0.14 && < 0.25
                , data-default
                , deepseq        >= 1.3  && < 1.5
                , hspec          >= 2.0  && < 3.0
                , servant-auth-cookie
-               , servant-server >= 0.5  && < 0.11
+               , servant-server >= 0.5  && < 0.12
+               , transformers   >= 0.4   && < 0.6
                , time           >= 1.5  && < 1.8.1
   if !impl(ghc >= 7.8)
     build-depends:    tagged    == 0.8.*
@@ -106,21 +118,26 @@
   if flag(build-examples)
     build-depends: base           >= 4.7  && < 5.0
                  , base-compat    >= 0.9.1 && <0.10
+                 , base64-bytestring
                  , blaze-html     >= 0.8  && < 0.10
                  , blaze-markup   >= 0.7  && < 0.9
                  , bytestring
                  , cereal         >= 0.5  && < 0.6
-                 , cryptonite     >= 0.14 && < 0.23
+                 , cryptonite     >= 0.14 && < 0.25
                  , data-default
+                 , directory
                  , exceptions
+                 , filepath
                  , http-media
+                 , http-types
                  , mtl            >= 2.0  && < 3.0
-                 , servant        >= 0.5  && < 0.11
+                 , servant        >= 0.5  && < 0.12
                  , servant-auth-cookie
                  , servant-blaze  >= 0.5  && < 0.10
-                 , servant-server >= 0.5  && < 0.11
+                 , servant-server >= 0.5  && < 0.12
                  , text
-                 , transformers   >= 0.4   && < 0.6
+                 , time
+                 , transformers   >= 0.4  && < 0.6
                  , wai            >= 3.0  && < 3.3
                  , warp           >= 3.0  && < 3.3
     if flag(servant9)
@@ -153,25 +170,29 @@
   if flag(build-examples)
     build-depends: base           >= 4.7  && < 5.0
                  , base-compat    >= 0.9.1 && <0.10
+                 , base64-bytestring
                  , blaze-markup
                  , blaze-html     >= 0.8  && < 0.10
                  , bytestring
                  , cereal         >= 0.5  && < 0.6
                  , exceptions
-                 , cryptonite     >= 0.14 && < 0.23
+                 , cryptonite     >= 0.14 && < 0.25
                  , data-default
                  , deepseq        >= 1.3  && < 1.5
+                 , directory
+                 , filepath
                  , http-media
                  , http-types
                  , hspec          >= 2.0  && < 3.0
                  , hspec-wai
+                 , mtl            >= 2.0  && < 3.0
                  , QuickCheck     >= 2.4  && < 3.0
                  , servant-auth-cookie
                  , servant-blaze  >= 0.5  && < 0.10
-                 , servant-server >= 0.5  && < 0.11
+                 , servant-server >= 0.5  && < 0.12
                  , text
                  , time           >= 1.5  && < 1.8.1
-                 , transformers   >= 0.4   && < 0.6
+                 , transformers   >= 0.4  && < 0.6
                  , wai
                  , wai-extra
     if flag(servant9)
@@ -179,9 +200,14 @@
         servant >= 0.9,
         http-api-data == 0.3.*
     else
-      build-depends:
-        servant < 0.9,
-        bytestring-conversion >= 0.3.1 && <0.4
+      if flag(servant91)
+        build-depends:
+          servant >= 0.9.1,
+          http-api-data == 0.3.*
+      else
+        build-depends:
+          servant < 0.9,
+          bytestring-conversion >= 0.3.1 && <0.4
 
     if !impl(ghc >= 7.8)
       build-depends:    tagged    == 0.8.*
@@ -201,10 +227,10 @@
 
   build-depends: base           >= 4.7     && < 5.0
                , bytestring
-               , criterion      >= 0.6.2.1 && < 1.2
-               , cryptonite     >= 0.14    && < 0.23
+               , criterion      >= 0.6.2.1 && < 1.3
+               , cryptonite     >= 0.14    && < 0.25
                , servant-auth-cookie
-               , servant-server >= 0.5     && < 0.11
+               , servant-server >= 0.5     && < 0.12
   if flag(dev)
     ghc-options:      -Wall -Werror
   else
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/servant-auth-cookie-0.4.4/src/Servant/Server/Experimental/Auth/Cookie.hs 
new/servant-auth-cookie-0.5.0.5/src/Servant/Server/Experimental/Auth/Cookie.hs
--- 
old/servant-auth-cookie-0.4.4/src/Servant/Server/Experimental/Auth/Cookie.hs    
    2017-04-15 11:57:55.000000000 +0200
+++ 
new/servant-auth-cookie-0.5.0.5/src/Servant/Server/Experimental/Auth/Cookie.hs  
    2017-07-13 10:40:23.000000000 +0200
@@ -23,6 +23,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections       #-}
 {-# LANGUAGE TypeFamilies        #-}
+{-# LANGUAGE RankNTypes          #-}
 
 module Servant.Server.Experimental.Auth.Cookie
   ( CipherAlgorithm
@@ -30,14 +31,26 @@
   , Cookie (..)
   , AuthCookieException (..)
 
+  , WithMetadata (..)
+#if MIN_VERSION_servant(0,9,1)
+  , Cookied
+  , cookied
+#endif
+
   , RandomSource
   , mkRandomSource
   , getRandomBytes
+  , generateRandomBytes
 
   , ServerKey
-  , mkServerKey
-  , mkServerKeyFromBytes
-  , getServerKey
+  , ServerKeySet (..)
+
+  , PersistentServerKey
+  , mkPersistentServerKey
+
+  , RenewableKeySet
+  , RenewableKeySetHooks (..)
+  , mkRenewableKeySet
 
   , AuthCookieSettings (..)
 
@@ -56,16 +69,16 @@
   , removeSessionFromErr
   , getSession
 
+  , defaultAuthHandler
+
   -- exposed for testing purpose
   , renderSession
   , parseSessionRequest
   , parseSessionResponse
-
-  , defaultAuthHandler
   ) where
 
 import Blaze.ByteString.Builder (toByteString)
-import Control.Arrow ((&&&))
+import Control.Arrow ((&&&), first)
 import Control.Monad
 import Control.Monad.Catch (MonadThrow (..), Exception)
 import Control.Monad.Except
@@ -75,11 +88,12 @@
 import Crypto.Hash (HashAlgorithm(..))
 import Crypto.Hash.Algorithms (SHA256)
 import Crypto.MAC.HMAC (HMAC)
-import Crypto.Random (drgNew, DRG(..))
+import Crypto.Random (DRG(..), drgNew)
 import Data.ByteString (ByteString)
 import Data.Default
 import Data.IORef
-import Data.Maybe (fromMaybe)
+import Data.List (partition)
+import Data.Maybe (listToMaybe)
 import Data.Monoid ((<>))
 import Data.Proxy
 import Data.Serialize
@@ -112,14 +126,22 @@
 import Data.ByteString.Conversion (ToByteString (..))
 #endif
 
+#if MIN_VERSION_servant(0,9,1)
+import Servant (noHeader, Handler)
+import Servant.API.ResponseHeaders (Headers)
+import qualified Servant.API.Header as S(Header)
+#endif
+
 #if MIN_VERSION_http_types(0,9,2)
 import Network.HTTP.Types (hSetCookie)
+#endif
+
+#if MIN_VERSION_http_types(0,9,2)
 #else
 hSetCookie :: HeaderName
 hSetCookie = "Set-Cookie"
 #endif
 
-
 ----------------------------------------------------------------------------
 -- General types
 
@@ -128,7 +150,14 @@
 
 -- | A type family that maps user-defined data to 'AuthServerData'.
 type family AuthCookieData
-type instance AuthServerData (AuthProtect "cookie-auth") = AuthCookieData
+
+-- | Wrapper for cookies and sessions to keep some related metadata.
+data WithMetadata a = WithMetadata
+  { wmData  :: a     -- ^ Value itself
+  , wmRenew :: Bool  -- ^ Whether we should renew cookies/session
+  }
+
+type instance AuthServerData (AuthProtect "cookie-auth") = WithMetadata 
AuthCookieData
 
 -- | Cookie representation.
 data Cookie = Cookie
@@ -154,8 +183,12 @@
   builder (EncryptedSession s) = builder s
 #endif
 
--- | The exception is thrown when something goes wrong with this package.
+#if MIN_VERSION_servant(0,9,1)
+-- | Helper type to wrap endpoints.
+type Cookied a = Headers '[S.Header "Set-Cookie" EncryptedSession] a
+#endif
 
+-- | The exception is thrown when something goes wrong with this package.
 data AuthCookieException
   = CannotMakeIV ByteString
     -- ^ Could not make 'IV' for block cipher.
@@ -203,8 +236,8 @@
 
 -- | Constructor for 'RandomSource' value.
 mkRandomSource :: (MonadIO m, DRG d)
-  => IO d              -- ^ How to get deterministic random generator
-  -> Int -- ^ Threshold (number of bytes to be generated before resetting)
+  => IO d           -- ^ How to get deterministic random generator
+  -> Int            -- ^ Threshold (number of bytes to be generated before 
resetting)
   -> m RandomSource -- ^ New 'RandomSource' value
 mkRandomSource mkDRG threshold =
   RandomSource mkDRG threshold `liftM` liftIO ((,0) <$> mkDRG >>= newIORef)
@@ -226,74 +259,103 @@
 ----------------------------------------------------------------------------
 -- Server key
 
--- | A mutable state of ServerKey.
-data ServerKeyState = ServerKeyState
-  { sksBytes :: ByteString
-    -- ^ Current value of the key
-  , sksExpirationTime :: UTCTime
-    -- ^ When the key is expires
+-- | Internal representation of a server key.
+type ServerKey = ByteString
+
+-- | Interface for a set of server keys.
+class ServerKeySet k where
+  getKeys   :: (MonadThrow m, MonadIO m) => k -> m (ServerKey, [ServerKey])
+  -- ^ Retrieve current and rotated keys respectively.
+
+  removeKey :: (MonadThrow m, MonadIO m) => k -> ServerKey -> m ()
+  -- ^ Non-graciously remove the key from a keyset.
+
+
+-- | A keyset containing only one key, that doesn't change.
+data PersistentServerKey = PersistentServerKey
+  { pskBytes :: ServerKey }
+
+instance ServerKeySet PersistentServerKey where
+  getKeys     = return . (,[]) . pskBytes
+  removeKey _ = error "removeKey @PersistentServerKey: not implemented"
+
+-- | Create instance of 'PersistentServerKey'.
+mkPersistentServerKey :: ByteString -> PersistentServerKey
+mkPersistentServerKey bytes = PersistentServerKey { pskBytes = bytes }
+
+
+-- | Customizable actions for 'RenewableKeySet'.
+data RenewableKeySetHooks s p = RenewableKeySetHooks
+  { rkshNewState :: forall m. (MonadIO m, MonadThrow m)
+    => p                  -- KeySet parameters
+    -> ([ServerKey], s)   -- Current state
+    -> m ([ServerKey], s) -- New state
+    -- ^ Called when a keyset needs to refresh it's state. It's result might be
+    -- discarded occasionally in favour of result yielded in another thread.
+
+  , rkshNeedUpdate :: forall m. (MonadIO m, MonadThrow m)
+    => p                 -- KeySet parameters
+    -> ([ServerKey], s)  -- Current state
+    -> m Bool            -- Whether to update the state
+    -- ^ Called before retrieving the keys and refreshing the state.
+
+  , rkshRemoveKey :: forall m. (MonadIO m, MonadThrow m)
+    => p          -- KeySet parameters
+    -> ServerKey  -- Key to remove
+    -> m ()       -- Action to perform
+    -- ^ Called after removing the key. This hook is called only if the key
+    -- belongs to a keyset and called once per key. The only purpose of it is
+    -- to clear the garbage after removing the key. The state might differs
+    -- after removing the key and before calling the hook, therefore the hook
+    -- doesn't rely on the state.
   }
 
--- | A wrapper of self-resetting 'ByteString' of random symbols suitable for
--- concurrent usage.
-data ServerKey = ServerKey
-  { skSize :: Int
-    -- ^ Size of the key (in bytes)
-  , skMaxAge :: Maybe NominalDiffTime
-    -- ^ Expiration time ('Nothing' is enternity)
-  , skState :: IORef ServerKeyState
-    -- ^ Mutable state of the key
+
+-- | Customizable key set, that provides partial implementation of
+-- 'ServerKeySet'.
+data RenewableKeySet s p = RenewableKeySet
+  { rksState      :: IORef ([ServerKey], s)
+    -- ^ Key set state (keys and user-defined state).
+
+  , rksParameters :: p
+    -- ^ User-defined parameters of the key set.
+
+  , rksHooks      :: RenewableKeySetHooks s p
+    -- ^ USer-defined hooks of the key set.
   }
 
--- | Constructor for 'ServerKey' value.
-mkServerKey :: MonadIO m
-  => Int               -- ^ Size of the server key
-  -> Maybe NominalDiffTime -- ^ Expiration time ('Nothing' is eternity)
-  -> m ServerKey       -- ^ New 'ServerKey'
-mkServerKey skSize skMaxAge = liftIO $ do
-  skState <- mkServerKeyState skSize skMaxAge >>= newIORef
-  return ServerKey {..}
-
--- | Constructor for 'ServerKey' value using predefined key.
-mkServerKeyFromBytes :: MonadIO m
-  => ByteString     -- ^ Predefined key
-  -> m ServerKey    -- ^ New 'ServerKey'
-mkServerKeyFromBytes bytes = liftIO $ do
-  let skSize = BS.length bytes
-  let skMaxAge = Nothing
-  skState <- newIORef ServerKeyState
-    { sksBytes = bytes
-    , sksExpirationTime = UTCTime (toEnum 0) 0
-      -- we don't care about the time as the key never expires
-    }
-  return ServerKey {..}
+instance (Eq s) => ServerKeySet (RenewableKeySet s p) where
+  getKeys RenewableKeySet {..} = getKeys' rksHooks where
+    getKeys' RenewableKeySetHooks {..} = do
+      state <- liftIO $ readIORef rksState
+      rkshNeedUpdate rksParameters state
+        >>= \needUpdate -> if not needUpdate
+          then return $ toResult state
+          else do
+            state' <- rkshNewState rksParameters state
+            liftIO $ atomicModifyIORef' rksState $ \state'' -> id &&& toResult 
$
+              if (userState state /= userState state'')
+              then state''
+              else state'
+    toResult = (head &&& tail) . fst
+    userState = snd
+
+  removeKey RenewableKeySet {..} key = do
+    found <- liftIO $ atomicModifyIORef' rksState $ \(keys, s) -> let
+      (found, keys') = first (not . null) . partition (== key) $ keys
+      in ((keys', s), found)
+    when found $ (rkshRemoveKey rksHooks) rksParameters key
+
+-- | Create instance of 'RenewableKeySet'.
+mkRenewableKeySet :: (MonadIO m)
+  => RenewableKeySetHooks s p -- ^ Hooks
+  -> p                        -- ^ Parameters
+  -> s                        -- ^ Initial state
+  -> m (RenewableKeySet s p)
+mkRenewableKeySet rksHooks rksParameters userState = liftIO $ do
+  rksState <- newIORef ([], userState)
+  return RenewableKeySet {..}
 
--- | Extract value from 'ServerKey'.
-getServerKey :: MonadIO m
-  => ServerKey         -- ^ The 'ServerKey'
-  -> m ByteString      -- ^ Its random symbol
-getServerKey ServerKey {..} = liftIO $ maybe
-  (sksBytes <$> readIORef skState)
-  (\_ -> do
-    currentTime <- getCurrentTime
-    state <- readIORef skState
-    case (currentTime > sksExpirationTime state) of
-      False -> return $ sksBytes state
-      True  -> do
-        state' <- mkServerKeyState skSize skMaxAge
-        atomicModifyIORef' skState $ \state'' -> id &&& sksBytes $
-          if (sksBytes state == sksBytes state'') then state' else state'')
-  skMaxAge
-
--- | An initializer of 'ServerKey' state.
-mkServerKeyState
-  :: Int               -- ^ Size of the server key
-  -> Maybe NominalDiffTime -- ^ Expiration time ('Nothing' is eternity)
-  -> IO ServerKeyState
-mkServerKeyState skSize skMaxAge = do
-  sksBytes  <- fst . randomBytesGenerate skSize <$> drgNew
-  sksExpirationTime <- addUTCTime (fromMaybe 0 skMaxAge) <$> getCurrentTime
-  return ServerKeyState {..}
 
 ----------------------------------------------------------------------------
 -- Settings
@@ -347,18 +409,18 @@
 --     * 'TooShortProperKey'
 --     * 'CannotMakeIV'
 --     * 'BadProperKey'
-encryptCookie :: (MonadIO m, MonadThrow m)
+encryptCookie :: (MonadIO m, MonadThrow m, ServerKeySet k)
   => AuthCookieSettings -- ^ Options, see 'AuthCookieSettings'
-  -> ServerKey         -- ^ 'ServerKey' to use
-  -> Cookie            -- ^ The 'Cookie' to encrypt
+  -> k                  -- ^ Instance of 'ServerKeySet' to use
+  -> Cookie             -- ^ The 'Cookie' to encrypt
   -> m (Tagged EncryptedCookie ByteString)  -- ^ Encrypted 'Cookie' is form of 
'ByteString'
-encryptCookie AuthCookieSettings {..} sk cookie = do
+encryptCookie AuthCookieSettings {..} sks cookie = do
   let iv = cookieIV cookie
       expiration = BSC8.pack $ formatTime
         defaultTimeLocale
         acsExpirationFormat
         (cookieExpirationTime cookie)
-  serverKey <- getServerKey sk
+  (serverKey, _) <- getKeys sks
   key <- mkProperKey
     (cipherKeySize $ unProxy acsCipher)
     (sign acsHashAlgorithm serverKey $ iv <> expiration)
@@ -383,12 +445,12 @@
 --     * 'IncorrectMAC'
 --     * 'CannotParseExpirationTime'
 --     * 'CookieExpired'
-decryptCookie :: (MonadIO m, MonadThrow m)
+decryptCookie :: (MonadIO m, MonadThrow m, ServerKeySet k)
   => AuthCookieSettings                 -- ^ Options, see 'AuthCookieSettings'
-  -> ServerKey                          -- ^ 'ServerKey' to use
+  -> k                                  -- ^ Instance of 'ServerKeySet' to use
   -> Tagged EncryptedCookie ByteString  -- ^ The 'ByteString' to decrypt
-  -> m Cookie                           -- ^ The decrypted 'Cookie'
-decryptCookie AuthCookieSettings {..} sk (Tagged s) = do
+  -> m (WithMetadata Cookie)            -- ^ The decrypted 'Cookie'
+decryptCookie AuthCookieSettings {..} sks (Tagged s) = do
   currentTime <- liftIO getCurrentTime
   let ivSize  = blockSize (unProxy acsCipher)
       expSize =
@@ -399,9 +461,16 @@
       (iv,            s0) = BS.splitAt ivSize s
       (expirationRaw, s1) = BS.splitAt expSize s0
       (payloadRaw,   mac) = BS.splitAt payloadSize s1
-  serverKey <- getServerKey sk
-  when (mac /= sign acsHashAlgorithm serverKey (BS.take butMacSize s)) $
-    throwM (IncorrectMAC mac)
+      checkMac sk = mac == sign acsHashAlgorithm sk (BS.take butMacSize s)
+
+  (currentKey, rotatedKeys) <- getKeys sks
+  (serverKey, renew) <- if checkMac currentKey
+    then return (currentKey, False)
+    else liftM (,True) $ maybe
+      (throwM $ IncorrectMAC mac)
+      (return)
+      (listToMaybe . map fst . filter snd . map (id &&& checkMac) $ 
rotatedKeys)
+
   expirationTime <-
     maybe (throwM $ CannotParseExpirationTime expirationRaw) return $
       parseTimeM False defaultTimeLocale acsExpirationFormat
@@ -412,21 +481,25 @@
     (cipherKeySize (unProxy acsCipher))
     (sign acsHashAlgorithm serverKey $ BS.take (ivSize + expSize) s)
   payload <- applyCipherAlgorithm acsDecryptAlgorithm iv key payloadRaw
-  return Cookie
-    { cookieIV             = iv
-    , cookieExpirationTime = expirationTime
-    , cookiePayload        = payload }
+  let cookie = Cookie
+        { cookieIV             = iv
+        , cookieExpirationTime = expirationTime
+        , cookiePayload        = payload }
+  return WithMetadata
+    { wmData = cookie
+    , wmRenew = renew
+    }
 
 ----------------------------------------------------------------------------
 -- Encrypt/decrypt session
 
 -- | Pack session object into a cookie. The function can throw the same
 -- exceptions as 'encryptCookie'.
-encryptSession :: (MonadIO m, MonadThrow m, Serialize a)
+encryptSession :: (MonadIO m, MonadThrow m, Serialize a, ServerKeySet k)
   => AuthCookieSettings -- ^ Options, see 'AuthCookieSettings'
-  -> RandomSource      -- ^ Random source to use
-  -> ServerKey         -- ^ 'ServerKey' to use
-  -> a                 -- ^ Session value
+  -> RandomSource       -- ^ Random source to use
+  -> k                  -- ^ Instance of 'ServerKeySet' to use
+  -> a                  -- ^ Session value
   -> m (Tagged SerializedEncryptedCookie ByteString)  -- ^ Serialized and 
encrypted session
 encryptSession acs@AuthCookieSettings {..} randomSource sk session = do
   iv <- getRandomBytes randomSource (blockSize $ unProxy acsCipher)
@@ -444,16 +517,18 @@
 
 -- | Unpack session value from a cookie. The function can throw the same
 -- exceptions as 'decryptCookie'.
-decryptSession :: (MonadIO m, MonadThrow m, Serialize a)
+decryptSession :: (MonadIO m, MonadThrow m, Serialize a, ServerKeySet k)
   => AuthCookieSettings                           -- ^ Options, see 
'AuthCookieSettings'
-  -> ServerKey                                    -- ^ 'ServerKey' to use
+  -> k                                            -- ^ Instance of 
'ServerKeySet' to use
   -> Tagged SerializedEncryptedCookie ByteString  -- ^ Cookie in binary form
-  -> m a                                          -- ^ Unpacked session value
-decryptSession acs@AuthCookieSettings {..} sk s =
+  -> m (WithMetadata a)                           -- ^ Unpacked session value
+decryptSession acs@AuthCookieSettings {..} sks s =
   let fromRight = either (throwM . SessionDeserializationFailed) return
   in fromRight (base64Decode s) >>=
-     decryptCookie acs sk        >>=
-     fromRight . runGet get . cookiePayload
+     decryptCookie acs sks      >>=
+     \w -> do
+        session <- fromRight . runGet get . cookiePayload $ wmData w
+        return w { wmData = session }
 
 ----------------------------------------------------------------------------
 -- Add/remove session
@@ -464,13 +539,14 @@
   :: ( MonadIO m
      , MonadThrow m
      , Serialize a
-     , AddHeader (e :: Symbol) EncryptedSession s r )
+     , AddHeader (e :: Symbol) EncryptedSession s r
+     , ServerKeySet k )
   => AuthCookieSettings -- ^ Options, see 'AuthCookieSettings'
-  -> RandomSource     -- ^ Random source to use
-  -> ServerKey         -- ^ 'ServerKey' to use
-  -> a                 -- ^ The session value
-  -> s                 -- ^ Response to add session to
-  -> m r               -- ^ Response with the session added
+  -> RandomSource       -- ^ Random source to use
+  -> k                  -- ^ Instance of 'ServerKeySet' to use
+  -> a                  -- ^ The session value
+  -> s                  -- ^ Response to add session to
+  -> m r                -- ^ Response with the session added
 addSession acs rs sk sessionData response = do
   header <- renderSession acs rs sk sessionData
   return (addHeader (EncryptedSession header) response)
@@ -490,12 +566,13 @@
 addSessionToErr
   :: ( MonadIO m
      , MonadThrow m
-     , Serialize a )
+     , Serialize a
+     , ServerKeySet k )
   => AuthCookieSettings -- ^ Options, see 'AuthCookieSettings'
-  -> RandomSource      -- ^ Random source to use
-  -> ServerKey         -- ^ 'ServerKey' to use
-  -> a                 -- ^ The session value
-  -> ServantErr        -- ^ Servant error to add the cookie to
+  -> RandomSource       -- ^ Random source to use
+  -> k                  -- ^ Instance of 'ServerKeySet' to use
+  -> a                  -- ^ The session value
+  -> ServantErr         -- ^ Servant error to add the cookie to
   -> m ServantErr
 addSessionToErr acs rs sk sessionData err = do
   header <- renderSession acs rs sk sessionData
@@ -528,11 +605,11 @@
 -- | Request handler that checks cookies. If 'Cookie' is just missing, you
 -- get 'Nothing', but if something is wrong with its format, 'getSession'
 -- can throw the same exceptions as 'decryptSession'.
-getSession :: (MonadIO m, MonadThrow m, Serialize a)
-  => AuthCookieSettings -- ^ Options, see 'AuthCookieSettings'
-  -> ServerKey         -- ^ 'ServerKey' to use
-  -> Request           -- ^ The request
-  -> m (Maybe a)       -- ^ The result
+getSession :: (MonadIO m, MonadThrow m, Serialize a, ServerKeySet k)
+  => AuthCookieSettings         -- ^ Options, see 'AuthCookieSettings'
+  -> k                          -- ^ 'ServerKeySet' to use
+  -> Request                    -- ^ The request
+  -> m (Maybe (WithMetadata a)) -- ^ The result
 getSession acs@AuthCookieSettings {..} sk request = maybe
   (return Nothing)
   (liftM Just . decryptSession acs sk)
@@ -565,10 +642,11 @@
 renderSession
   :: ( MonadIO m
      , MonadThrow m
-     , Serialize a )
+     , Serialize a
+     , ServerKeySet k )
   => AuthCookieSettings
   -> RandomSource
-  -> ServerKey
+  -> k
   -> a
   -> m ByteString
 renderSession acs@AuthCookieSettings {..} rs sk sessionData = do
@@ -581,14 +659,28 @@
       n = floor :: NominalDiffTime -> Int
   (return . toByteString . renderCookies) cookies
 
+
+#if MIN_VERSION_servant(0,9,1)
+-- | Wrapper for an implementation of an endpoint to make it automatically
+-- renew the cookies.
+cookied :: (Serialize a, ServerKeySet k)
+  => AuthCookieSettings                        -- ^ Options, see 
'AuthCookieSettings'
+  -> RandomSource                              -- ^ Random source to use
+  -> k                                         -- ^ Instance of 'ServerKeySet' 
to use
+  -> (a -> r)                                  -- ^ Implementation of an 
endpoint
+  -> ((WithMetadata a) -> Handler (Cookied r)) -- ^ "Cookied" endpoint
+cookied acs rs k f = \(WithMetadata {..}) ->
+  (if wmRenew then addSession acs rs k wmData else (return . noHeader)) $ f 
wmData
+#endif
+
 ----------------------------------------------------------------------------
 -- Default auth handler
 
 -- | Cookie authentication handler.
-defaultAuthHandler :: Serialize a
-  => AuthCookieSettings -- ^ Options, see 'AuthCookieSettings'
-  -> ServerKey         -- ^ 'ServerKey' to use
-  -> AuthHandler Request a -- ^
+defaultAuthHandler :: (Serialize a, ServerKeySet k)
+  => AuthCookieSettings                   -- ^ Options, see 
'AuthCookieSettings'
+  -> k                                    -- ^ Instance of 'ServerKeySet' to 
use
+  -> AuthHandler Request (WithMetadata a) -- ^ The result
 defaultAuthHandler acs sk = mkAuthHandler $ \request -> do
   msession <- liftIO (getSession acs sk request)
   maybe (throwError err403) return msession
@@ -599,9 +691,9 @@
 -- | Applies 'H.hmac' algorithm to given data.
 sign :: forall h. HashAlgorithm h
   => Proxy h           -- ^ The hash algorithm to use
-  -> ByteString        -- ^
-  -> ByteString
-  -> ByteString
+  -> ByteString        -- ^ The key
+  -> ByteString        -- ^ The message
+  -> ByteString        -- ^ The result
 sign Proxy key msg = BA.convert (H.hmac key msg :: HMAC h)
 {-# INLINE sign #-}
 
@@ -649,3 +741,8 @@
 
 unProxy :: Proxy a -> a
 unProxy Proxy = undefined
+
+-- | Generates random sequence of bytes from new DRG
+generateRandomBytes :: Int -> IO ByteString
+generateRandomBytes size = (fst . randomBytesGenerate size <$> drgNew)
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/servant-auth-cookie-0.4.4/tests/Main.hs 
new/servant-auth-cookie-0.5.0.5/tests/Main.hs
--- old/servant-auth-cookie-0.4.4/tests/Main.hs 2017-04-15 11:57:55.000000000 
+0200
+++ new/servant-auth-cookie-0.5.0.5/tests/Main.hs       2017-07-13 
10:40:23.000000000 +0200
@@ -8,6 +8,7 @@
 module Main (main) where
 
 import           Control.Concurrent                      (threadDelay)
+import           Control.Monad.IO.Class                  (MonadIO, liftIO)
 import           Crypto.Cipher.AES                       (AES128, AES192,
                                                           AES256)
 import           Crypto.Cipher.Types
@@ -34,10 +35,11 @@
 
 spec :: Spec
 spec = do
-  describe "RandomSource" randomSourceSpec
-  describe "ServerKey"    serverKeySpec
-  describe "Cookie"       cookieSpec
-  describe "Session"      sessionSpec
+  describe "RandomSource"        randomSourceSpec
+  describe "PersistentServerKey" persistentServerKeySpec
+  describe "RenewalKeySet"       renewalKeySetSpec
+  describe "Cookie"              cookieSpec
+  describe "Session"             sessionSpec
 
 randomSourceSpec :: Spec
 randomSourceSpec = do
@@ -61,33 +63,81 @@
       s2 <- getRandomBytes rs 10
       s1 `shouldNotBe` s2
 
-serverKeySpec :: Spec
-serverKeySpec = do
-  context "when creating a new server key" $
+persistentServerKeySpec :: Spec
+persistentServerKeySpec = do
+  context "when creating a new persistent server key" $ do
+    let keySize = 64
+    let getSK = mkPersistentServerKey <$> generateRandomBytes keySize
+            >>= getKeys
+
     it "has correct size" $ do
-      let keySize = 64
-      sk <- mkServerKey keySize Nothing
-      k  <- getServerKey sk
+      (k, _) <- getSK
       BS.length k `shouldNotBe` (keySize `div` 8)
-  context "when creating a new server key from data" $
-    it "has data as server key" $ do
-      let bytes = "0123456789"
-      sk <- mkServerKeyFromBytes bytes
-      k  <- getServerKey sk
-      k `shouldBe` bytes
-  context "until expiration" $
-    it "returns the same key" $ do
-      sk <- mkServerKey 16 Nothing
-      k0 <- getServerKey sk
-      k1 <- getServerKey sk
-      k0 `shouldBe` k1
-  context "when a key expires" $
-    it "is reset" $ do
-      sk <- mkServerKey 16 (Just $ fromIntegral (1 :: Integer))
-      k1 <- getServerKey sk
-      threadDelay 2000000
-      k2 <- getServerKey sk
-      k1 `shouldNotBe` k2
+
+    it "has no rotated keys" $ do
+      (_, ks) <- getSK
+      length ks `shouldBe` 0
+
+
+renewalKeySetSpec :: Spec
+renewalKeySetSpec = spec' where
+
+  keySize :: Int
+  keySize = 16
+
+  rkshNewState :: (MonadIO m)
+    => NominalDiffTime
+    -> ([ServerKey], UTCTime)
+    -> m ([ServerKey], UTCTime)
+  rkshNewState _ (keys, _) = liftIO $ (,)
+    <$> (fmap (:keys) $ generateRandomBytes keySize)
+    <*> getCurrentTime
+
+  rkshNeedUpdate :: (MonadIO m)
+    => NominalDiffTime
+    -> ([ServerKey], UTCTime)
+    -> m Bool
+  rkshNeedUpdate dt (_, t) = liftIO $ getCurrentTime >>= return . ((dt 
`addUTCTime` t) <)
+
+  rkshRemoveKey :: (MonadIO m)
+    => NominalDiffTime
+    -> ServerKey
+    -> m ()
+  rkshRemoveKey _ _ = return ()
+
+  spec' = do
+    let makeSK = mkRenewableKeySet
+          RenewableKeySetHooks {..}
+          (fromIntegral (1 :: Integer))
+          (UTCTime (toEnum 0) 0)
+
+    context "when accessing a renewable key set" $ do
+      it "updates the keys when needed" $ do
+        sk <- makeSK
+
+        (k, ks) <- getKeys sk
+        BS.length k `shouldNotBe` 0
+
+        (_, ks') <- threadDelay 1500000 >> getKeys sk
+        ks' `shouldBe` (k:ks)
+
+      it "doesn't update the keys when not needed" $ do
+        sk <- makeSK
+        k  <- getKeys sk
+        k' <- getKeys sk
+        k' `shouldBe` k
+
+    context "when removing key from a renewable key set" $ do
+      it "removes specified key" $ do
+        sk <- makeSK
+        (_, ks) <- getKeys sk >> threadDelay 1500000 >> getKeys sk
+        length ks `shouldNotBe` 0
+
+        let k = head ks
+        removeKey sk k
+        (_, ks') <- getKeys sk
+        (k:ks') `shouldBe` ks
+
 
 cookieSpec :: Spec
 cookieSpec = do
@@ -179,7 +229,8 @@
   -> (BS.ByteString -> BS.ByteString) -- ^ Encryption hook
   -> IO Cookie         -- ^ Restored 'Cookie'
 cipherId h c encryptAlgorithm decryptAlgorithm cookie encryptionHook = do
-  sk     <- mkServerKey 16 Nothing
+  sk <- mkPersistentServerKey <$> generateRandomBytes 16
+
   let sts =
         case def of
           AuthCookieSettings {..} -> AuthCookieSettings
@@ -188,7 +239,7 @@
             , acsHashAlgorithm    = h
             , acsCipher           = c
             , .. }
-  encryptCookie sts sk cookie >>= decryptCookie sts sk . fmap encryptionHook
+  encryptCookie sts sk cookie >>= (fmap wmData . decryptCookie sts sk . fmap 
encryptionHook)
 
 sessionSpec :: Spec
 sessionSpec = do
@@ -229,8 +280,8 @@
   -> IO (Tree a)
 encryptThenDecrypt _ settings x = do
   rs <- mkRandomSource drgNew 1000
-  sk <- mkServerKey 16 Nothing
-  encryptSession settings rs sk x >>= decryptSession settings sk
+  sk <- mkPersistentServerKey <$> generateRandomBytes 16
+  encryptSession settings rs sk x >>= (fmap wmData . decryptSession settings 
sk)
 
 data Tree a = Leaf a | Node a [Tree a] deriving (Eq, Show, Generic)
 
@@ -246,3 +297,4 @@
   oneof
     [ Leaf <$> arbitrary
     , Node <$> arbitrary <*> vectorOf l (arbitraryTree (n `quot` 2))]
+


Reply via email to