Do not require jwt secret, but die on auth without it

This commit is contained in:
Joe Nelson
2016-09-24 21:28:08 -07:00
parent 449480bf01
commit 62ed9e2c4d
9 changed files with 69 additions and 23 deletions
+2 -2
View File
@@ -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 }
+1
View File
@@ -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
View File
@@ -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
+6 -2
View File
@@ -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
+2 -2
View File
@@ -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
+3 -1
View File
@@ -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)
+24
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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