Skip to content
This repository has been archived by the owner on Oct 29, 2021. It is now read-only.

Followup on #54: Make XSRF optional #92

Merged
merged 11 commits into from May 1, 2018
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,9 @@ checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do
checkCreds _ _ _ = throwError err401
~~~

### CSRF and the frontend
### XSRF and the frontend

CSRF protection works by requiring that there be a header of the same value as
XSRF protection works by requiring that there be a header of the same value as
a distinguished cookie that is set by the server on each request. What the
cookie and header name are can be configured (see `xsrfCookieName` and
`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and
Expand All @@ -247,10 +247,19 @@ $.ajaxPrefilter(function(opts, origOpts, xhr) {

~~~


I *believe* nothing at all needs to be done if you're using Angular's `$http`
directive, but I haven't tested this.

XSRF protection can be disabled just for `GET` requests by setting
`xsrfExcludeGet = False`. You might want this if you're relying on the browser
to navigate between pages that require cookie authentication.

XSRF protection can be completely disabled by setting `cookieXsrfSetting =
Nothing` in `CookieSettings`. This is not recommended! If your cookie
authenticated web application runs any javascript, it's recommended to send the
XSRF header. However, if your web application runs no javascript, disabling
XSRF entirely may be required.

# Note on this README

This README is a literate haskell file. Here is 'main', allowing you to pick
Expand Down
7 changes: 7 additions & 0 deletions servant-auth-server/CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
upcoming:

releases:
- version: *unreleased*
- description: extract 'XsrfCookieSettings' from 'CookieSettings' and make XSRF checking optional.
notes: >
Renamed 'makeCsrfCookie' to 'makeXsrfCookie' and marked the former as deprecated.
Made several changes to the structure of 'CookieSettings' which will require attention by users who have modified the XSRF settings.
authors: 3noch, plredmond
pr: *not yet PR'd*
- version: 0.3.0.0
changes:
- description: 'cookiePath' and 'xsrfCookiePath' added to 'CookieSettings'
Expand Down
15 changes: 12 additions & 3 deletions servant-auth-server/executables/README.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,9 @@ checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do
checkCreds _ _ _ = throwError err401
~~~

### CSRF and the frontend
### XSRF and the frontend

CSRF protection works by requiring that there be a header of the same value as
XSRF protection works by requiring that there be a header of the same value as
a distinguished cookie that is set by the server on each request. What the
cookie and header name are can be configured (see `xsrfCookieName` and
`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and
Expand All @@ -247,10 +247,19 @@ $.ajaxPrefilter(function(opts, origOpts, xhr) {

~~~


I *believe* nothing at all needs to be done if you're using Angular's `$http`
directive, but I haven't tested this.

XSRF protection can be disabled just for `GET` requests by setting
`xsrfExcludeGet = False`. You might want this if you're relying on the browser
to navigate between pages that require cookie authentication.

XSRF protection can be completely disabled by setting `cookieXsrfSetting =
Nothing` in `CookieSettings`. This is not recommended! If your cookie
authenticated web application runs any javascript, it's recommended to send the
XSRF header. However, if your web application runs no javascript, disabling
XSRF entirely may be required.

# Note on this README

This README is a literate haskell file. Here is 'main', allowing you to pick
Expand Down
1 change: 1 addition & 0 deletions servant-auth-server/servant-auth-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ test-suite spec
, http-types
, wai
, servant-server
, transformers

-- test dependencies
build-depends:
Expand Down
4 changes: 4 additions & 0 deletions servant-auth-server/src/Servant/Auth/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@ module Servant.Auth.Server

-- ** Settings
, CookieSettings(..)
, XsrfCookieSettings(..)
, defaultCookieSettings
, defaultXsrfCookieSettings
, makeSessionCookie
, makeSessionCookieBS
, makeXsrfCookie
, makeCsrfCookie
, makeCookie
, makeCookieBS
, acceptLogin
, clearSession


-- ** Related types
Expand Down
4 changes: 2 additions & 2 deletions servant-auth-server/src/Servant/Auth/Server/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ instance ( n ~ 'S ('S 'Z)

makeCookies :: AuthResult v -> IO (SetCookieList ('S ('S 'Z)))
makeCookies authResult = do
csrf <- makeCsrfCookie cookieSettings
fmap (Just csrf `SetCookieCons`) $
xsrf <- makeXsrfCookie cookieSettings
fmap (Just xsrf `SetCookieCons`) $
case authResult of
(Authenticated v) -> do
ejwt <- makeSessionCookie cookieSettings jwtSettings v
Expand Down
53 changes: 34 additions & 19 deletions servant-auth-server/src/Servant/Auth/Server/Internal/ConfigTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import qualified Data.ByteString as BS
import Data.Default.Class
import Data.Time
import GHC.Generics (Generic)
import Network.Wai (Request)

data IsMatch = Matches | DoesNotMatch
deriving (Eq, Show, Read, Generic, Ord)
Expand Down Expand Up @@ -48,25 +49,19 @@ data CookieSettings = CookieSettings
{
-- | 'Secure' means browsers will only send cookies over HTTPS. Default:
-- @Secure@.
cookieIsSecure :: IsSecure
cookieIsSecure :: !IsSecure
-- | How long from now until the cookie expires. Default: @Nothing@.
, cookieMaxAge :: Maybe DiffTime
, cookieMaxAge :: !(Maybe DiffTime)
-- | At what time the cookie expires. Default: @Nothing@.
, cookieExpires :: Maybe UTCTime
, cookieExpires :: !(Maybe UTCTime)
-- | The URL path and sub-paths for which this cookie is used. Default @Just "/"@.
, cookiePath :: Maybe BS.ByteString
, cookiePath :: !(Maybe BS.ByteString)
-- | 'SameSite' settings. Default: @SameSiteLax@.
, cookieSameSite :: SameSite
, cookieSameSite :: !SameSite
-- | What name to use for the cookie used for the session.
, sessionCookieName :: BS.ByteString
-- | What name to use for the cookie used for CSRF protection.
, xsrfCookieName :: BS.ByteString
-- | What path to use for the cookie used for CSRF protection. Default @Just "/"@.
, xsrfCookiePath :: Maybe BS.ByteString
-- | What name to use for the header used for CSRF protection.
, xsrfHeaderName :: BS.ByteString
-- | Exclude GET request method from CSRF protection.
, xsrfExcludeGet :: Bool
, sessionCookieName :: !BS.ByteString
-- | The optional settings to use for XSRF protection. Default @Just def@.
, cookieXsrfSetting :: !(Maybe XsrfCookieSettings)
} deriving (Eq, Show, Generic)

instance Default CookieSettings where
Expand All @@ -80,12 +75,32 @@ defaultCookieSettings = CookieSettings
, cookiePath = Just "/"
, cookieSameSite = SameSiteLax
, sessionCookieName = "JWT-Cookie"
, xsrfCookieName = "XSRF-TOKEN"
, xsrfCookiePath = Just "/"
, xsrfHeaderName = "X-XSRF-TOKEN"
, xsrfExcludeGet = False
, cookieXsrfSetting = Just def
}

-- | The policies to use when generating and verifying XSRF cookies
data XsrfCookieSettings = XsrfCookieSettings
{
-- | What name to use for the cookie used for XSRF protection.
xsrfCookieName :: !BS.ByteString
-- | What path to use for the cookie used for XSRF protection. Default @Just "/"@.
, xsrfCookiePath :: !(Maybe BS.ByteString)
-- | What name to use for the header used for XSRF protection.
, xsrfHeaderName :: !BS.ByteString
-- | Exclude GET request method from XSRF protection.
, xsrfExcludeGet :: !Bool
} deriving (Eq, Show, Generic)

instance Default XsrfCookieSettings where
def = defaultXsrfCookieSettings

defaultXsrfCookieSettings :: XsrfCookieSettings
defaultXsrfCookieSettings = XsrfCookieSettings
{ xsrfCookieName = "XSRF-TOKEN"
, xsrfCookiePath = Just "/"
, xsrfHeaderName = "X-XSRF-TOKEN"
, xsrfExcludeGet = False
}

------------------------------------------------------------------------------
-- Internal {{{
Expand All @@ -94,6 +109,6 @@ jwtSettingsToJwtValidationSettings :: JWTSettings -> Jose.JWTValidationSettings
jwtSettingsToJwtValidationSettings s
= defaultJWTValidationSettings (toBool <$> audienceMatches s)
where
toBool Matches = True
toBool Matches = True
toBool DoesNotMatch = False
-- }}}
121 changes: 88 additions & 33 deletions servant-auth-server/src/Servant/Auth/Server/Internal/Cookie.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import qualified Crypto.JOSE as Jose
import qualified Crypto.JWT as Jose
import Crypto.Util (constTimeEq)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Base64 as BS64
import qualified Data.ByteString.Lazy as BSL
import Data.CaseInsensitive (mk)
import Data.Maybe (fromMaybe, isJust)
import Network.HTTP.Types (methodGet)
import Network.Wai (requestHeaders, requestMethod)
import Network.HTTP.Types.Header(hCookie)
import Network.Wai (Request, requestHeaders, requestMethod)
import Servant (AddHeader, addHeader)
import System.Entropy (getEntropy)
import Web.Cookie
Expand All @@ -26,12 +29,12 @@ cookieAuthCheck :: FromJWT usr => CookieSettings -> JWTSettings -> AuthCheck usr
cookieAuthCheck ccfg jwtCfg = do
req <- ask
jwtCookie <- maybe mempty return $ do
cookies' <- lookup "Cookie" $ requestHeaders req
cookies' <- lookup hCookie $ requestHeaders req
let cookies = parseCookies cookies'
xsrfCookie <- lookup (xsrfCookieName ccfg) cookies
when (((requestMethod req) /= methodGet) || not (xsrfExcludeGet ccfg)) $ do
xsrfHeader <- lookup (mk $ xsrfHeaderName ccfg) $ requestHeaders req
guard $ xsrfCookie `constTimeEq` xsrfHeader
-- Apply the XSRF check if enabled.
guard $ fromMaybe True $ do
xsrfCookieCfg <- xsrfCheckRequired ccfg req
return $ xsrfCookieAuthCheck xsrfCookieCfg req cookies
-- session cookie *must* be HttpOnly and Secure
lookup (sessionCookieName ccfg) cookies
verifiedJWT <- liftIO $ runExceptT $ do
Expand All @@ -45,41 +48,80 @@ cookieAuthCheck ccfg jwtCfg = do
Left _ -> mzero
Right v' -> return v'

-- | Makes a cookie to be used for CSRF.
xsrfCheckRequired :: CookieSettings -> Request -> Maybe XsrfCookieSettings
xsrfCheckRequired cookieSettings req = do
xsrfCookieCfg <- cookieXsrfSetting cookieSettings
let disableForGetReq = xsrfExcludeGet xsrfCookieCfg && requestMethod req == methodGet
guard $ not disableForGetReq
return xsrfCookieCfg

xsrfCookieAuthCheck :: XsrfCookieSettings -> Request -> [(BS.ByteString, BS.ByteString)] -> Bool
xsrfCookieAuthCheck xsrfCookieCfg req cookies = fromMaybe False $ do
xsrfCookie <- lookup (xsrfCookieName xsrfCookieCfg) cookies
xsrfHeader <- lookup (mk $ xsrfHeaderName xsrfCookieCfg) $ requestHeaders req
return $ xsrfCookie `constTimeEq` xsrfHeader

-- | Makes a cookie to be used for XSRF.
makeXsrfCookie :: CookieSettings -> IO SetCookie
makeXsrfCookie cookieSettings = case cookieXsrfSetting cookieSettings of
Just xsrfCookieSettings -> makeRealCookie xsrfCookieSettings
Nothing -> return $ noXsrfTokenCookie cookieSettings
where
makeRealCookie xsrfCookieSettings = do
xsrfValue <- BS64.encode <$> getEntropy 32
return
$ applyXsrfCookieSettings xsrfCookieSettings
$ applyCookieSettings cookieSettings
$ def{ setCookieValue = xsrfValue }


-- | Alias for 'makeXsrfCookie'.
makeCsrfCookie :: CookieSettings -> IO SetCookie
makeCsrfCookie cookieSettings = do
csrfValue <- BS64.encode <$> getEntropy 32
return $ def
{ setCookieName = xsrfCookieName cookieSettings
, setCookieValue = csrfValue
, setCookieMaxAge = cookieMaxAge cookieSettings
, setCookieExpires = cookieExpires cookieSettings
, setCookiePath = xsrfCookiePath cookieSettings
, setCookieSecure = case cookieIsSecure cookieSettings of
Secure -> True
NotSecure -> False
}
makeCsrfCookie = makeXsrfCookie
{-# DEPRECATED makeCsrfCookie "Use makeXsrfCookie instead" #-}


-- | Makes a cookie with session information.
makeSessionCookie :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe SetCookie)
makeSessionCookie cookieSettings jwtSettings v = do
ejwt <- makeJWT v jwtSettings Nothing
case ejwt of
Left _ -> return Nothing
Right jwt -> return $ Just $ def
{ setCookieName = sessionCookieName cookieSettings
, setCookieValue = BSL.toStrict jwt
, setCookieHttpOnly = True
, setCookieMaxAge = cookieMaxAge cookieSettings
, setCookieExpires = cookieExpires cookieSettings
, setCookiePath = cookiePath cookieSettings
, setCookieSecure = case cookieIsSecure cookieSettings of
Secure -> True
NotSecure -> False
}
Right jwt -> return
$ Just
$ applySessionCookieSettings cookieSettings
$ applyCookieSettings cookieSettings
$ def{ setCookieValue = BSL.toStrict jwt }

noXsrfTokenCookie :: CookieSettings -> SetCookie
noXsrfTokenCookie cookieSettings =
applyCookieSettings cookieSettings $ def{ setCookieName = "NO-XSRF-TOKEN", setCookieValue = "" }

applyCookieSettings :: CookieSettings -> SetCookie -> SetCookie
applyCookieSettings cookieSettings setCookie = setCookie
{ setCookieMaxAge = cookieMaxAge cookieSettings
, setCookieExpires = cookieExpires cookieSettings
, setCookiePath = cookiePath cookieSettings
, setCookieSecure = case cookieIsSecure cookieSettings of
Secure -> True
NotSecure -> False
}

applyXsrfCookieSettings :: XsrfCookieSettings -> SetCookie -> SetCookie
applyXsrfCookieSettings xsrfCookieSettings setCookie = setCookie
{ setCookieName = xsrfCookieName xsrfCookieSettings
, setCookiePath = xsrfCookiePath xsrfCookieSettings
, setCookieHttpOnly = False
}

applySessionCookieSettings :: CookieSettings -> SetCookie -> SetCookie
applySessionCookieSettings cookieSettings setCookie = setCookie
{ setCookieName = sessionCookieName cookieSettings
, setCookieHttpOnly = True
}

-- | For a JWT-serializable session, returns a function that decorates a
-- provided response object with CSRF and session cookies. This should be used
-- provided response object with XSRF and session cookies. This should be used
-- when a user successfully authenticates with credentials.
acceptLogin :: ( ToJWT session
, AddHeader "Set-Cookie" SetCookie response withOneCookie
Expand All @@ -93,8 +135,21 @@ acceptLogin cookieSettings jwtSettings session = do
case mSessionCookie of
Nothing -> pure Nothing
Just sessionCookie -> do
csrfCookie <- makeCsrfCookie cookieSettings
return $ Just $ addHeader sessionCookie . addHeader csrfCookie
xsrfCookie <- makeXsrfCookie cookieSettings
return $ Just $ addHeader sessionCookie . addHeader xsrfCookie

-- | Adds headers to a response that clears all session cookies.
clearSession :: ( AddHeader "Set-Cookie" SetCookie response withOneCookie
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
=> CookieSettings
-> response
-> withTwoCookies
clearSession cookieSettings = addHeader clearedSessionCookie . addHeader clearedXsrfCookie
where
clearedSessionCookie = applySessionCookieSettings cookieSettings $ applyCookieSettings cookieSettings def
clearedXsrfCookie = case cookieXsrfSetting cookieSettings of
Just xsrfCookieSettings -> applyXsrfCookieSettings xsrfCookieSettings $ applyCookieSettings cookieSettings def
Nothing -> noXsrfTokenCookie cookieSettings

makeSessionCookieBS :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe BS.ByteString)
makeSessionCookieBS a b c = fmap (toByteString . renderSetCookie) <$> makeSessionCookie a b c
Expand Down
Loading