Fix expired JWTs starting an empty transaction
Fixes https://github.com/PostgREST/postgrest/issues/1094. Expired JWTs were doing an empty BEGIN/COMMIT in the db.
This commit is contained in:
committed by
Steve Chavez
parent
a5bc293372
commit
55b4f4fbe7
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- #1530, Fix how the PostgREST version is shown in the help text when the `.git` directory is not available - @monacoremo
|
- #1530, Fix how the PostgREST version is shown in the help text when the `.git` directory is not available - @monacoremo
|
||||||
|
- #1094, Fix expired JWTs starting an empty transaction on the db - @steve-chavez
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
+21
-17
@@ -42,8 +42,8 @@ import Network.Wai
|
|||||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||||
InvokeMethod (..), Target (..),
|
InvokeMethod (..), Target (..),
|
||||||
mutuallyAgreeable, userApiRequest)
|
mutuallyAgreeable, userApiRequest)
|
||||||
import PostgREST.Auth (containsRole, jwtClaims,
|
import PostgREST.Auth (attemptJwtClaims, containsRole,
|
||||||
parseSecret)
|
jwtClaims, parseSecret)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.DbRequestBuilder (mutateRequest, readRequest,
|
import PostgREST.DbRequestBuilder (mutateRequest, readRequest,
|
||||||
returningCols)
|
returningCols)
|
||||||
@@ -79,26 +79,30 @@ postgrest conf refDbStructure pool getTime worker =
|
|||||||
Nothing -> respond . errorResponseFor $ ConnectionLostError
|
Nothing -> respond . errorResponseFor $ ConnectionLostError
|
||||||
Just dbStructure -> do
|
Just dbStructure -> do
|
||||||
response <- do
|
response <- do
|
||||||
-- Need to parse ?columns early because findProc needs it to solve overloaded functions.
|
|
||||||
-- TODO: move this logic to the app function
|
|
||||||
let apiReq = userApiRequest (configSchemas conf) (configRootSpec conf) req body
|
let apiReq = userApiRequest (configSchemas conf) (configRootSpec conf) req body
|
||||||
|
-- Need to parse ?columns early because findProc needs it to solve overloaded functions.
|
||||||
apiReqCols = (,) <$> apiReq <*> (pRequestColumns . iColumns =<< apiReq)
|
apiReqCols = (,) <$> apiReq <*> (pRequestColumns . iColumns =<< apiReq)
|
||||||
case apiReqCols of
|
case apiReqCols of
|
||||||
Left err -> return . errorResponseFor $ err
|
Left err -> return . errorResponseFor $ err
|
||||||
Right (apiRequest, maybeCols) -> do
|
Right (apiRequest, maybeCols) -> do
|
||||||
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf)
|
-- The jwt must be checked before touching the db.
|
||||||
let authed = containsRole eClaims
|
attempt <- attemptJwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf)
|
||||||
cols = case (iPayload apiRequest, maybeCols) of
|
case jwtClaims attempt of
|
||||||
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
|
Left errJwt -> return . errorResponseFor $ errJwt
|
||||||
(Just RawJSON{}, Just cls) -> cls
|
Right claims -> do
|
||||||
_ -> S.empty
|
let
|
||||||
proc = case iTarget apiRequest of
|
authed = containsRole claims
|
||||||
TargetProc qi _ -> findProc qi cols (iPreferParameters apiRequest == Just SingleObject) $ dbProcs dbStructure
|
cols = case (iPayload apiRequest, maybeCols) of
|
||||||
_ -> Nothing
|
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure proc cols conf) apiRequest
|
(Just RawJSON{}, Just cls) -> cls
|
||||||
txMode = transactionMode proc (iAction apiRequest)
|
_ -> S.empty
|
||||||
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
|
proc = case iTarget apiRequest of
|
||||||
return $ either (errorResponseFor . PgError authed) identity response
|
TargetProc qi _ -> findProc qi cols (iPreferParameters apiRequest == Just SingleObject) $ dbProcs dbStructure
|
||||||
|
_ -> Nothing
|
||||||
|
handleReq = runPgLocals conf claims (app dbStructure proc cols conf) apiRequest
|
||||||
|
txMode = transactionMode proc (iAction apiRequest)
|
||||||
|
dbResp <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
|
||||||
|
return $ either (errorResponseFor . PgError authed) identity dbResp
|
||||||
when (responseStatus response == status503) worker
|
when (responseStatus response == status503) worker
|
||||||
respond response
|
respond response
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -15,7 +15,7 @@ very simple authentication system inside the PostgreSQL database.
|
|||||||
module PostgREST.Auth (
|
module PostgREST.Auth (
|
||||||
containsRole
|
containsRole
|
||||||
, jwtClaims
|
, jwtClaims
|
||||||
, JWTAttempt(..)
|
, attemptJwtClaims
|
||||||
, parseSecret
|
, parseSecret
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ import Data.Time.Clock (UTCTime)
|
|||||||
import Control.Lens.Operators
|
import Control.Lens.Operators
|
||||||
import Crypto.JWT
|
import Crypto.JWT
|
||||||
|
|
||||||
|
import PostgREST.Error (SimpleError (..))
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
import Protolude hiding (toS)
|
import Protolude hiding (toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
@@ -42,13 +43,22 @@ data JWTAttempt = JWTInvalid JWTError
|
|||||||
| JWTClaims (M.HashMap Text JSON.Value)
|
| JWTClaims (M.HashMap Text JSON.Value)
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
|
||||||
|
jwtClaims :: JWTAttempt -> Either SimpleError (M.HashMap Text JSON.Value)
|
||||||
|
jwtClaims attempt =
|
||||||
|
case attempt of
|
||||||
|
JWTMissingSecret -> Left JwtTokenMissing
|
||||||
|
JWTInvalid JWTExpired -> Left $ JwtTokenInvalid "JWT expired"
|
||||||
|
JWTInvalid e -> Left $ JwtTokenInvalid $ show e
|
||||||
|
JWTClaims claims -> Right claims
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Receives the JWT secret and audience (from config) and a JWT and returns a map
|
Receives the JWT secret and audience (from config) and a JWT and returns a map
|
||||||
of JWT claims.
|
of JWT claims.
|
||||||
-}
|
-}
|
||||||
jwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
|
attemptJwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
|
||||||
jwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
|
attemptJwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
|
||||||
jwtClaims secret audience payload time jspath =
|
attemptJwtClaims secret audience payload time jspath =
|
||||||
case secret of
|
case secret of
|
||||||
Nothing -> return JWTMissingSecret
|
Nothing -> return JWTMissingSecret
|
||||||
Just s -> do
|
Just s -> do
|
||||||
@@ -82,9 +92,8 @@ walkJSPath _ _ = Nothing
|
|||||||
{-|
|
{-|
|
||||||
Whether a response from jwtClaims contains a role claim
|
Whether a response from jwtClaims contains a role claim
|
||||||
-}
|
-}
|
||||||
containsRole :: JWTAttempt -> Bool
|
containsRole :: M.HashMap Text JSON.Value -> Bool
|
||||||
containsRole (JWTClaims claims) = M.member "role" claims
|
containsRole = M.member "role"
|
||||||
containsRole _ = False
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Parse `jwt-secret` configuration option and turn into a JWKSet.
|
Parse `jwt-secret` configuration option and turn into a JWKSet.
|
||||||
|
|||||||
+22
-31
@@ -19,43 +19,34 @@ import Network.Wai.Middleware.Cors (cors)
|
|||||||
import Network.Wai.Middleware.Gzip (def, gzip)
|
import Network.Wai.Middleware.Gzip (def, gzip)
|
||||||
import Network.Wai.Middleware.Static (only, staticPolicy)
|
import Network.Wai.Middleware.Static (only, staticPolicy)
|
||||||
|
|
||||||
import Crypto.JWT
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..))
|
import PostgREST.ApiRequest (ApiRequest (..))
|
||||||
import PostgREST.Auth (JWTAttempt (..))
|
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
import PostgREST.Error (SimpleError (JwtTokenInvalid, JwtTokenMissing),
|
|
||||||
errorResponseFor)
|
|
||||||
import PostgREST.QueryBuilder (setLocalQuery, setLocalSearchPathQuery)
|
import PostgREST.QueryBuilder (setLocalQuery, setLocalSearchPathQuery)
|
||||||
import Protolude hiding (head, toS)
|
import Protolude hiding (head, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
runWithClaims :: AppConfig -> JWTAttempt ->
|
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
||||||
(ApiRequest -> H.Transaction Response) ->
|
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
|
||||||
ApiRequest -> H.Transaction Response
|
(ApiRequest -> H.Transaction Response) ->
|
||||||
runWithClaims conf eClaims app req =
|
ApiRequest -> H.Transaction Response
|
||||||
case eClaims of
|
runPgLocals conf claims app req = do
|
||||||
JWTMissingSecret -> return . errorResponseFor $ JwtTokenMissing
|
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql
|
||||||
JWTInvalid JWTExpired -> return . errorResponseFor . JwtTokenInvalid $ "JWT expired"
|
traverse_ H.sql customReqCheck
|
||||||
JWTInvalid e -> return . errorResponseFor . JwtTokenInvalid . show $ e
|
app req
|
||||||
JWTClaims claims -> do
|
where
|
||||||
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql
|
methodSql = setLocalQuery mempty ("request.method", toS $ iMethod req)
|
||||||
mapM_ H.sql customReqCheck
|
pathSql = setLocalQuery mempty ("request.path", toS $ iPath req)
|
||||||
app req
|
headersSql = setLocalQuery "request.header." <$> iHeaders req
|
||||||
where
|
cookiesSql = setLocalQuery "request.cookie." <$> iCookies req
|
||||||
methodSql = setLocalQuery mempty ("request.method", toS $ iMethod req)
|
claimsSql = setLocalQuery "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
||||||
pathSql = setLocalQuery mempty ("request.path", toS $ iPath req)
|
appSettingsSql = setLocalQuery mempty <$> configSettings conf
|
||||||
headersSql = setLocalQuery "request.header." <$> iHeaders req
|
setRoleSql = maybeToList $ (\x ->
|
||||||
cookiesSql = setLocalQuery "request.cookie." <$> iCookies req
|
setLocalQuery mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
|
||||||
claimsSql = setLocalQuery "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
setSearchPathSql = setLocalSearchPathQuery (iSchema req : configExtraSearchPath conf)
|
||||||
appSettingsSql = setLocalQuery mempty <$> configSettings conf
|
-- role claim defaults to anon if not specified in jwt
|
||||||
setRoleSql = maybeToList $ (\x ->
|
claimsWithRole = M.union claims (M.singleton "role" anon)
|
||||||
setLocalQuery mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
|
anon = JSON.String . toS $ configAnonRole conf
|
||||||
setSearchPathSql = setLocalSearchPathQuery (iSchema req : configExtraSearchPath conf)
|
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
|
||||||
-- role claim defaults to anon if not specified in jwt
|
|
||||||
claimsWithRole = M.union claims (M.singleton "role" anon)
|
|
||||||
anon = JSON.String . toS $ configAnonRole conf
|
|
||||||
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
|
|
||||||
|
|
||||||
defaultMiddle :: Application -> Application
|
defaultMiddle :: Application -> Application
|
||||||
defaultMiddle =
|
defaultMiddle =
|
||||||
|
|||||||
Reference in New Issue
Block a user