nested-routes-6.0.0: examples/STM/Auth.hs
{-# LANGUAGE
FlexibleContexts
, OverloadedStrings
#-}
module STM.Auth where
import Network.Wai.Trans
import Network.HTTP.Types
import Web.Cookie
import Data.Attoparsec.Text
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as BS
import Blaze.ByteString.Builder (toByteString)
import qualified Data.IntMap as IntMap
import Data.ByteString.UTF8 (fromString)
import Data.ByteArray (convert)
import Data.Time
import Data.Time.ISO8601
import Data.Maybe
import Control.Concurrent.STM
import Control.Error.Util
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.State
import Crypto.Hash
-- | @sec@
data SecurityLayer = ShouldBeLoggedIn
| ShouldBeModerator
| ShouldBeAdmin
deriving (Show, Eq, Ord)
data AuthEnv = AuthEnv
{ authEnvCache :: TVar (IntMap.IntMap (UTCTime, BS.ByteString))
, authEnvFreshId :: TVar Int
, authEnvSalt :: BS.ByteString
}
sessionId :: BS.ByteString
sessionId = "security-id"
sessionNonce :: BS.ByteString
sessionNonce = "security-token"
sessionTime :: BS.ByteString
sessionTime = "security-token-time"
makeSessionCookies :: UserSession BS.ByteString -> RequestHeaders
makeSessionCookies (UserSession i t c) = repeat "Set-Cookie" `zip` cookies
where
cookies =
[ toByteString $
renderSetCookie $ def { setCookieName = sessionId
, setCookieMaxAge = Just 60
, setCookieValue = fromString $ show i }
, toByteString $
renderSetCookie $ def { setCookieName = sessionTime
, setCookieMaxAge = Just 60
, setCookieValue = T.encodeUtf8 $ T.pack $ formatISO8601 t }
, toByteString $
renderSetCookie $ def { setCookieName = sessionNonce
, setCookieMaxAge = Just 60
, setCookieValue = c }
]
clearSessionResponse :: Response -> Response
clearSessionResponse = mapResponseHeaders $
filter $ \(a,b) ->
let cookieName = setCookieName $ parseSetCookie b
in not $ a == "Set-Cookie" && ( cookieName == sessionId
|| cookieName == sessionTime
|| cookieName == sessionNonce )
clearSessionRequest :: Request -> Request
clearSessionRequest req =
req {requestHeaders = go $ requestHeaders req}
where
go = filter $ \(a,b) ->
let cookies = parseCookies b
hasSession (k,_) = k == sessionId || k == sessionTime || k == sessionNonce
in not $ a == "Cookie" && any hasSession cookies
getUserSession :: RequestHeaders -> Maybe (UserSession BS.ByteString)
getUserSession hs =
let (mi,mt,mc) = foldr go (Nothing,Nothing,Nothing) hs
in UserSession <$> mi <*> mt <*> mc
where
go (k,c) (mi,mt,mc) | k == "Cookie" =
let cs = parseCookies c
go' (k',c') (mi',mt',mc')
| k' == sessionId = (hush $ parseOnly integer $ T.decodeUtf8 c', mt', mc')
| k' == sessionTime = (mi', parseISO8601 $ T.unpack $ T.decodeUtf8 c', mc')
| k' == sessionNonce = (mi', mt', Just c')
| otherwise = (mi',mt',mc')
in foldr go' (mi,mt,mc) cs
| otherwise = (mi,mt,mc)
integer :: Parser Int
integer = do
xs <- double
return $ floor xs
data UserSession a = UserSession
{ userSessID :: Int
, userSessTime :: UTCTime
, userSessNonce :: a
} deriving (Show, Eq, Ord)
newUserSession :: ( MonadIO m
, MonadReader AuthEnv m
) => m (UserSession BS.ByteString)
newUserSession = do
salt <- authEnvSalt <$> ask
uIdKey <- authEnvFreshId <$> ask
uId <- liftIO $ atomically $ do
i <- readTVar uIdKey
modifyTVar' uIdKey (+1)
return i
now <- liftIO getCurrentTime
return $ UserSession uId now $ BS.encode $ convert $ go now salt
where
go :: UTCTime -> BS.ByteString -> Digest SHA512
go now salt = hash $ salt <> fromString (formatISO8601 now)
lookupUserSession :: ( MonadIO m
, MonadReader AuthEnv m
) => Int -> m (Maybe (UserSession BS.ByteString))
lookupUserSession i = do
cacheKey <- authEnvCache <$> ask
liftIO $ atomically $ do
cache <- readTVar cacheKey
return $ do (t,c) <- IntMap.lookup i cache
return $ UserSession i t c
deleteUserSession :: ( MonadIO m
, MonadReader AuthEnv m
) => Int -> m ()
deleteUserSession i = do
cacheKey <- authEnvCache <$> ask
liftIO $ atomically $ modifyTVar' cacheKey (IntMap.delete i)
writeUserSession :: ( MonadIO m
, MonadReader AuthEnv m
) => UserSession BS.ByteString -> m ()
writeUserSession (UserSession i t c) = do
cacheKey <- authEnvCache <$> ask
liftIO $ atomically $ modifyTVar' cacheKey $ IntMap.insert i (t, c)
checkUserSession :: MonadReader AuthEnv m =>
UserSession BS.ByteString
-> m Bool
checkUserSession (UserSession _ t c) = do
salt <- authEnvSalt <$> ask
let old = hash $ salt <> fromString (formatISO8601 t) :: Digest SHA512
old' = BS.encode $ convert old :: BS.ByteString
return $ c == old'
data AuthenticationError = NoSessionHeaders
| InvalidRequestNonce
| SessionNotInCache
| SessionMismatch
| SessionTimedOut
authenticate :: ( MonadIO m
, MonadReader AuthEnv m
) => Request -> [SecurityLayer] -> m (Response -> Response, Maybe AuthenticationError)
authenticate _ [] = return (id, Nothing)
authenticate req _ = flip runStateT Nothing $ do
liftIO $ putStrLn "<!> Auth Attempt <!>"
liftIO $ putStrLn "--- Headers ----------- \n" >> print (requestHeaders req) >> putStrLn "\n"
let mUserSession = getUserSession $ requestHeaders req
put $ Just NoSessionHeaders <* mUserSession
maybe (return id) hasUserSession mUserSession
hasUserSession userSession = do
liftIO $ putStrLn "--- User Session ------ \n" >> print userSession >> putStrLn "\n"
cacheKey <- authEnvCache <$> ask
cache <- liftIO $ readTVarIO cacheKey
liftIO $ putStrLn "--- Cache ------------- \n" >> print cache >> putStrLn "\n"
mCachedSession <- lookupUserSession (userSessID userSession)
put $ Just SessionNotInCache <* mCachedSession
maybe (return id) (hasCachedSession userSession) mCachedSession
hasCachedSession userSession (UserSession i cachedTime cachedNonce) = do
newUserSess' <- newUserSession
let newUserSess = newUserSess' {userSessID = i}
userSessionIsValid <- checkUserSession userSession
case () of
() | diffUTCTime (userSessTime newUserSess) cachedTime >= 60 -> do
deleteUserSession i
put $ Just SessionTimedOut
return id
| cachedNonce /= userSessNonce userSession -> do
put $ Just SessionMismatch
return id
| not userSessionIsValid -> do
put $ Just InvalidRequestNonce
return id
| otherwise -> do
writeUserSession newUserSess
return $ mapResponseHeaders (++ makeSessionCookies newUserSess)
. clearSessionResponse
note' :: MonadError e m => e -> Maybe a -> m a
note' e mx = fromMaybe (throwError e) $ pure <$> mx
insert :: Eq k => k -> a -> [(k,a)] -> [(k,a)]
insert k v [] = [(k,v)]
insert k v ((k',v'):xs) | k == k' = (k,v):xs
| otherwise = (k',v'): insert k v xs