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:
steve-chavez
2020-07-03 17:23:10 -05:00
committed by Steve Chavez
parent a5bc293372
commit 55b4f4fbe7
4 changed files with 60 additions and 55 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 =