agrafix / Spock

Another Haskell web framework for rapid development
https://www.spock.li
679 stars 56 forks source link

Disable session cookie / alternate session implmentation #100

Open forficate opened 7 years ago

forficate commented 7 years ago

Two questions.

1) Is it possible to disable Spock from creating the spockcookie session cookie? I can see it's created with Wai middleware but can't an easy way to disable it.

2) This leads on from I want to use a encrypted cookie to store sessions and not use a remote session store.

I currently create a encrypted session cookie on a callback from Auth0 with a expiry field in the cookie, user id, name and roles. The expiry is low, each request returns a new session cookie with expiry incremented creating a sliding session.

I can see the backend session storage is plugable but no way to disable backend storage and use a client side implementation? Is anything on the roadmap to allow this?

agrafix commented 7 years ago

Let's say: It depends :-)

If you use the Spock-core package you have full control over that, the framework will not inject any cookies. But you also don't have CSRF protection built in, no global "state" and no database pooling. The latter two are easy to implement in your own monad stack on top of SpockT.

If you use the Spock package, you currently can not prevent Spock from creating a spockcookie. But I'd be happy to explore how we could integrate a fully client side session management, as I know of several other Spock users that have a similar setup. Are you interested in contributing?

forficate commented 7 years ago

Thanks.

I did switch to Spock-core using a ReaderT with my own Connection Pool and cookie Key from the clientsession package. Firefox was still reporting the spockcookie in the inspector after repeatedly deleting it.

Checking again it must of been a Firefox issue, the cookie is no longer appearing. Verifying with curl the Set-Cookie header is not set as expected.

I am new to Haskell / Spock, happy to contribute where I can if you have any implementation suggestions.

Not knowing to much it might be possible using the existing SessionManager type? sm_clearAllSessions and sm_closeSessionManager would be a noop for client side. sm_middleware could have a middleware which automatically increments/validates expiry based on some config value. Not sure about the need for a sessionId clientside but can stay for compatibility. m is the Spock monad so there is access to read / write a cookie?

data SessionManager m conn sess st
   = SessionManager
   { sm_getSessionId :: m SessionId
   , sm_getCsrfToken :: m T.Text
   , sm_regenerateSessionId :: m ()
   , sm_readSession :: m sess
   , sm_writeSession :: sess -> m ()
   , sm_modifySession :: forall a. (sess -> (sess, a)) -> m a
   , sm_mapSessions :: (forall n. Monad n => sess -> n sess) -> m ()
   , sm_clearAllSessions :: MonadIO m => m ()
   , sm_middleware :: Middleware
   , sm_closeSessionManager :: IO ()
   }
agrafix commented 7 years ago

Actually, the current SessionManager implements a session manager for all m in MonadIO (see https://github.com/agrafix/Spock/blob/138198fa5e50d7238bfecc64678403aadc80fc37/Spock/src/Web/Spock/Internal/SessionManager.hs#L43-L50 ). We should probably split the SessionManager into two (or three) types otherwise the type will not represent the semantics (i.E. sm_mapSessions can not be implemented for client side sessions.) We will also need to track if we use client or server side sessions in the sess parameter somehow we carry around to call/expose the functions to the end user. So to start we'd need to split the type for example like so:

data BaseSessionManager m conn sess st
   = BaseSessionManager
   { bsm_getSessionId :: m SessionId
   , bsm_getCsrfToken :: m T.Text
   , bsm_regenerateSessionId :: m ()
   , bsm_readSession :: m sess
   , bsm_writeSession :: sess -> m ()
   , bsm_modifySession :: forall a. (sess -> (sess, a)) -> m a
   , bsm_middleware :: Middleware
   }
data ServerSessionManager m conn sess st
   = ServerSessionManager
   { ssm_base :: m (BaseSessionManager m conn sess st)
   , ssm_mapSessions :: (forall n. Monad n => sess -> n sess) -> m ()
   , ssm_clearAllSessions :: MonadIO m => m ()
   , ssm_closeSessionManager :: IO ()
   }
data ClientSessionManager m conn sess st
   = ClientSessionManager
   { csm_base :: m (BaseSessionManager m conn sess st)
   , -- what do we need apart from that?
   }

All session values would then be wrapped with either newtype ServerSession sess = ServerSession sess and newtype ClientSession sess = ClientSession sess

The functions in https://github.com/agrafix/Spock/blob/138198fa5e50d7238bfecc64678403aadc80fc37/Spock/src/Web/Spock/SessionActions.hs would then move to type classes IsAnySession, IsServerSession and IsClientSession and be dependent on the session wrapper, as of how the session manager is initialized and chosen.

What do you think?