Do not require jwt secret, but die on auth without it
This commit is contained in:
+2
-2
@@ -88,7 +88,7 @@ main = do
|
|||||||
loadSecretFile :: AppConfig -> IO AppConfig
|
loadSecretFile :: AppConfig -> IO AppConfig
|
||||||
loadSecretFile conf = do
|
loadSecretFile conf = do
|
||||||
let s = configJwtSecret conf
|
let s = configJwtSecret conf
|
||||||
real <- case stripPrefix "@" s of
|
real <- case join (stripPrefix "@" <$> s) of
|
||||||
Nothing -> return s -- the string is the secret, not a filename
|
Nothing -> return s -- the string is the secret, not a filename
|
||||||
Just filename -> readFile (toS filename)
|
Just filename -> sequence . Just $ readFile (toS filename)
|
||||||
return conf { configJwtSecret = real }
|
return conf { configJwtSecret = real }
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ Test-Suite spec
|
|||||||
, Feature.CorsSpec
|
, Feature.CorsSpec
|
||||||
, Feature.DeleteSpec
|
, Feature.DeleteSpec
|
||||||
, Feature.InsertSpec
|
, Feature.InsertSpec
|
||||||
|
, Feature.NoJwtSpec
|
||||||
, Feature.QuerySpec
|
, Feature.QuerySpec
|
||||||
, Feature.QueryLimitedSpec
|
, Feature.QueryLimitedSpec
|
||||||
, Feature.RangeSpec
|
, Feature.RangeSpec
|
||||||
|
|||||||
+13
-8
@@ -80,7 +80,8 @@ postgrest conf refDbStructure pool =
|
|||||||
|
|
||||||
let schema = toS $ configSchema conf
|
let schema = toS $ configSchema conf
|
||||||
apiRequest = userApiRequest schema req body
|
apiRequest = userApiRequest schema req body
|
||||||
eClaims = jwtClaims (secret $ configJwtSecret conf) (iJWT apiRequest) time
|
eClaims = jwtClaims
|
||||||
|
(secret <$> configJwtSecret conf) (iJWT apiRequest) time
|
||||||
authed = containsRole eClaims
|
authed = containsRole eClaims
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
||||||
txMode = transactionMode $ iAction apiRequest
|
txMode = transactionMode $ iAction apiRequest
|
||||||
@@ -212,7 +213,7 @@ app dbStructure conf apiRequest =
|
|||||||
Just (PayloadJSON (UniformObjects payload))) -> do
|
Just (PayloadJSON (UniformObjects payload))) -> do
|
||||||
let p = V.head payload
|
let p = V.head payload
|
||||||
singular = iPreferSingular apiRequest
|
singular = iPreferSingular apiRequest
|
||||||
jwtSecret = secret $ configJwtSecret conf
|
jwtSecret = secret <$> configJwtSecret conf
|
||||||
returnType = lookup (qiName qi) $ dbProcs dbStructure
|
returnType = lookup (qiName qi) $ dbProcs dbStructure
|
||||||
returnsJWT = fromMaybe False $
|
returnsJWT = fromMaybe False $
|
||||||
isInfixOf "jwt_claims" . pdReturnType <$> returnType
|
isInfixOf "jwt_claims" . pdReturnType <$> returnType
|
||||||
@@ -220,13 +221,17 @@ app dbStructure conf apiRequest =
|
|||||||
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
||||||
Right (q,cq) -> respondToRange $ do
|
Right (q,cq) -> respondToRange $ do
|
||||||
row <- H.query () (callProc qi p q cq topLevelRange shouldCount singular)
|
row <- H.query () (callProc qi p q cq topLevelRange shouldCount singular)
|
||||||
let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row
|
let (tableTotal, queryTotal, body) =
|
||||||
|
fromMaybe (Just 0, 0, emptyArray) row
|
||||||
(status, contentRange) = rangeHeader queryTotal tableTotal
|
(status, contentRange) = rangeHeader queryTotal tableTotal
|
||||||
in
|
return $ case (returnsJWT, jwtSecret) of
|
||||||
return $ responseLBS status [jsonH, contentRange]
|
(True, Nothing) ->
|
||||||
(if returnsJWT
|
errResponse status500 "Server lacks JWT secret"
|
||||||
then "{\"token\":\"" <> toS (tokenJWT jwtSecret body) <> "\"}"
|
(True, Just s) ->
|
||||||
else toS $ encode body)
|
responseLBS status [jsonH, contentRange] $
|
||||||
|
"{\"token\":\"" <> toS (tokenJWT s body) <> "\"}"
|
||||||
|
(False, _) ->
|
||||||
|
responseLBS status [jsonH, contentRange] (toS . encode $ body)
|
||||||
|
|
||||||
(ActionRead, TargetRoot, Nothing) -> do
|
(ActionRead, TargetRoot, Nothing) -> do
|
||||||
let host = configHost conf
|
let host = configHost conf
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ claimsToSQL claims = roleStmts <> varStmts
|
|||||||
-}
|
-}
|
||||||
data JWTAttempt = JWTExpired
|
data JWTAttempt = JWTExpired
|
||||||
| JWTInvalid
|
| JWTInvalid
|
||||||
|
| JWTMissingSecret
|
||||||
| JWTClaims (M.HashMap Text Value)
|
| JWTClaims (M.HashMap Text Value)
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
@@ -59,9 +60,13 @@ data JWTAttempt = JWTExpired
|
|||||||
Receives the JWT secret (from config) and a JWT and returns a map
|
Receives the JWT secret (from config) and a JWT and returns a map
|
||||||
of JWT claims.
|
of JWT claims.
|
||||||
-}
|
-}
|
||||||
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt
|
jwtClaims :: Maybe JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt
|
||||||
jwtClaims _ "" _ = JWTClaims M.empty
|
jwtClaims _ "" _ = JWTClaims M.empty
|
||||||
jwtClaims secret jwt time =
|
jwtClaims secret jwt time =
|
||||||
|
case secret of
|
||||||
|
Nothing -> JWTMissingSecret
|
||||||
|
Just s ->
|
||||||
|
let mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature s jwt in
|
||||||
case isExpired <$> mClaims of
|
case isExpired <$> mClaims of
|
||||||
Just True -> JWTExpired
|
Just True -> JWTExpired
|
||||||
Nothing -> JWTInvalid
|
Nothing -> JWTInvalid
|
||||||
@@ -70,7 +75,6 @@ jwtClaims secret jwt time =
|
|||||||
isExpired claims =
|
isExpired claims =
|
||||||
let mExp = claims ^? key "exp" . _Integer
|
let mExp = claims ^? key "exp" . _Integer
|
||||||
in fromMaybe False $ (<= time) . fromInteger <$> mExp
|
in fromMaybe False $ (<= time) . fromInteger <$> mExp
|
||||||
mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature secret jwt
|
|
||||||
value2map (Object o) = o
|
value2map (Object o) = o
|
||||||
value2map _ = M.empty
|
value2map _ = M.empty
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ data AppConfig = AppConfig {
|
|||||||
, configSchema :: Text
|
, configSchema :: Text
|
||||||
, configHost :: Text
|
, configHost :: Text
|
||||||
, configPort :: Int
|
, configPort :: Int
|
||||||
, configJwtSecret :: Text
|
, configJwtSecret :: Maybe Text
|
||||||
, configPool :: Int
|
, configPool :: Int
|
||||||
, configMaxRows :: Maybe Integer
|
, configMaxRows :: Maybe Integer
|
||||||
, configQuiet :: Bool
|
, configQuiet :: Bool
|
||||||
@@ -54,7 +54,7 @@ argParser = AppConfig
|
|||||||
<*> (toS <$> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault))
|
<*> (toS <$> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault))
|
||||||
<*> (toS <$> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault))
|
<*> (toS <$> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault))
|
||||||
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
|
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
|
||||||
<*> (toS <$> strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET"))
|
<*> (optional . map toS <$> strOption) (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET")
|
||||||
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
|
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
|
||||||
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
|
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
|
||||||
<*> pure False
|
<*> pure False
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Data.Aeson (Value (..))
|
|||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Hasql.Transaction as H
|
import qualified Hasql.Transaction as H
|
||||||
|
|
||||||
import Network.HTTP.Types.Status (unauthorized401)
|
import Network.HTTP.Types.Status (unauthorized401, status500)
|
||||||
import Network.Wai (Application, Response,
|
import Network.Wai (Application, Response,
|
||||||
responseLBS)
|
responseLBS)
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
@@ -18,6 +18,7 @@ import PostgREST.ApiRequest (ApiRequest(..), ContentType(..),
|
|||||||
ctToHeader)
|
ctToHeader)
|
||||||
import PostgREST.Auth (claimsToSQL, JWTAttempt(..))
|
import PostgREST.Auth (claimsToSQL, JWTAttempt(..))
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
|
import PostgREST.Error (errResponse)
|
||||||
|
|
||||||
import Protolude hiding (concat, null)
|
import Protolude hiding (concat, null)
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ runWithClaims conf eClaims app req =
|
|||||||
case eClaims of
|
case eClaims of
|
||||||
JWTExpired -> return $ unauthed "JWT expired"
|
JWTExpired -> return $ unauthed "JWT expired"
|
||||||
JWTInvalid -> return $ unauthed "JWT invalid"
|
JWTInvalid -> return $ unauthed "JWT invalid"
|
||||||
|
JWTMissingSecret -> return $ errResponse status500 "Server lacks JWT secret"
|
||||||
JWTClaims claims -> do
|
JWTClaims claims -> do
|
||||||
-- role claim defaults to anon if not specified in jwt
|
-- role claim defaults to anon if not specified in jwt
|
||||||
H.sql . mconcat . claimsToSQL $ M.union claims (M.singleton "role" anon)
|
H.sql . mconcat . claimsToSQL $ M.union claims (M.singleton "role" anon)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
module Feature.NoJwtSpec where
|
||||||
|
|
||||||
|
-- {{{ Imports
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
import Network.Wai (Application)
|
||||||
|
-- }}}
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = describe "server started without JWT secret" $ do
|
||||||
|
|
||||||
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
|
it "responds with error on attempted auth" $ do
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` 500
|
||||||
|
|
||||||
|
it "responds with error when attempting to generate JWT token" $
|
||||||
|
post "/rpc/login" [json| { "id": "jdoe", "pass": "1234" } |]
|
||||||
|
`shouldRespondWith` 500
|
||||||
@@ -15,6 +15,7 @@ import qualified Feature.ConcurrentSpec
|
|||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
import qualified Feature.DeleteSpec
|
import qualified Feature.DeleteSpec
|
||||||
import qualified Feature.InsertSpec
|
import qualified Feature.InsertSpec
|
||||||
|
import qualified Feature.NoJwtSpec
|
||||||
import qualified Feature.QueryLimitedSpec
|
import qualified Feature.QueryLimitedSpec
|
||||||
import qualified Feature.QuerySpec
|
import qualified Feature.QuerySpec
|
||||||
import qualified Feature.RangeSpec
|
import qualified Feature.RangeSpec
|
||||||
@@ -34,6 +35,7 @@ main = do
|
|||||||
ltdApp = return $ postgrest testLtdRowsCfg refDbStructure pool
|
ltdApp = return $ postgrest testLtdRowsCfg refDbStructure pool
|
||||||
unicodeApp = return $ postgrest testUnicodeCfg refDbStructure pool
|
unicodeApp = return $ postgrest testUnicodeCfg refDbStructure pool
|
||||||
proxyApp = return $ postgrest testProxyCfg refDbStructure pool
|
proxyApp = return $ postgrest testProxyCfg refDbStructure pool
|
||||||
|
noJwtApp = return $ postgrest testCfgNoJWT refDbStructure pool
|
||||||
|
|
||||||
hspec $ do
|
hspec $ do
|
||||||
mapM_ (beforeAll_ resetDb . before withApp) specs
|
mapM_ (beforeAll_ resetDb . before withApp) specs
|
||||||
@@ -50,6 +52,10 @@ main = do
|
|||||||
beforeAll_ resetDb . before proxyApp $
|
beforeAll_ resetDb . before proxyApp $
|
||||||
describe "Feature.ProxySpec" Feature.ProxySpec.spec
|
describe "Feature.ProxySpec" Feature.ProxySpec.spec
|
||||||
|
|
||||||
|
-- this test runs without a JWT secret
|
||||||
|
beforeAll_ resetDb . before noJwtApp $
|
||||||
|
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
specs = map (uncurry describe) [
|
specs = map (uncurry describe) [
|
||||||
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
||||||
|
|||||||
+8
-4
@@ -51,19 +51,23 @@ testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_t
|
|||||||
|
|
||||||
testCfg :: AppConfig
|
testCfg :: AppConfig
|
||||||
testCfg =
|
testCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 "safe" 10 Nothing True
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 Nothing True
|
||||||
|
|
||||||
|
testCfgNoJWT :: AppConfig
|
||||||
|
testCfgNoJWT =
|
||||||
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 Nothing 10 Nothing True
|
||||||
|
|
||||||
testUnicodeCfg :: AppConfig
|
testUnicodeCfg :: AppConfig
|
||||||
testUnicodeCfg =
|
testUnicodeCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "تست" "localhost" 3000 "safe" 10 Nothing True
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "تست" "localhost" 3000 (Just "safe") 10 Nothing True
|
||||||
|
|
||||||
testLtdRowsCfg :: AppConfig
|
testLtdRowsCfg :: AppConfig
|
||||||
testLtdRowsCfg =
|
testLtdRowsCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 "safe" 10 (Just 2) True
|
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 (Just 2) True
|
||||||
|
|
||||||
testProxyCfg :: AppConfig
|
testProxyCfg :: AppConfig
|
||||||
testProxyCfg =
|
testProxyCfg =
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" (Just "https://postgrest.com/openapi.json") "test" "localhost" 3000 "safe" 10 Nothing True
|
AppConfig testDbConn "postgrest_test_anonymous" (Just "https://postgrest.com/openapi.json") "test" "localhost" 3000 (Just "safe") 10 Nothing True
|
||||||
|
|
||||||
setupDb :: IO ()
|
setupDb :: IO ()
|
||||||
setupDb = do
|
setupDb = do
|
||||||
|
|||||||
Reference in New Issue
Block a user