diff --git a/main/Main.hs b/main/Main.hs index 1ece9e28c..26eda3a44 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -28,6 +28,7 @@ import Network.Wai.Handler.Warp (defaultSettings, runSettings, import System.IO (BufferMode (..), hSetBuffering) import PostgREST.App (postgrest) +import PostgREST.Auth (parseSecret) import PostgREST.Config (AppConfig (..), configPoolTimeout', prettyVersion, readAppConfig, readPathShowHelp, loadDbUriFile, loadSecretFile) import PostgREST.DbStructure (getDbStructure, getPgVersion) @@ -203,7 +204,9 @@ main = do path <- readPathShowHelp -- build the 'AppConfig' from the config file path - conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig path + conf <- do + cnf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig path + pure cnf { configJWKS = parseSecret <$> configJwtSecret cnf} -- Checks that the provided proxy uri is formated correctly when (isMalformedProxyUri $ toS <$> configOpenAPIProxyUri conf) $ diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 2f03ca8d7..1d336c8f3 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -43,7 +43,7 @@ import PostgREST.ApiRequest (Action (..), ApiRequest (..), InvokeMethod (..), Target (..), mutuallyAgreeable, userApiRequest) import PostgREST.Auth (attemptJwtClaims, containsRole, - jwtClaims, parseSecret) + jwtClaims) import PostgREST.Config (AppConfig (..)) import PostgREST.DbRequestBuilder (mutateRequest, readRequest, returningCols) @@ -69,8 +69,7 @@ import Protolude.Conv (toS) postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application postgrest conf refDbStructure pool getTime worker = - let middle = (if configQuiet conf then id else logStdout) . defaultMiddle - jwtSecret = parseSecret <$> configJwtSecret conf in + let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in middle $ \ req respond -> do time <- getTime body <- strictRequestBody req @@ -86,7 +85,7 @@ postgrest conf refDbStructure pool getTime worker = Left err -> return . errorResponseFor $ err Right (apiRequest, maybeCols) -> do -- The jwt must be checked before touching the db. - attempt <- attemptJwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf) + attempt <- attemptJwtClaims (configJWKS conf) (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf) case jwtClaims attempt of Left errJwt -> return . errorResponseFor $ errJwt Right claims -> do diff --git a/src/PostgREST/Auth.hs b/src/PostgREST/Auth.hs index 600e733c9..0277c2919 100644 --- a/src/PostgREST/Auth.hs +++ b/src/PostgREST/Auth.hs @@ -58,14 +58,14 @@ jwtClaims attempt = -} attemptJwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt attemptJwtClaims _ _ "" _ _ = return $ JWTClaims M.empty -attemptJwtClaims secret audience payload time jspath = - case secret of +attemptJwtClaims maybeSecret audience payload time jspath = + case maybeSecret of Nothing -> return JWTMissingSecret - Just s -> do + Just secret -> do let validation = set allowedSkew 1 $ defaultJWTValidationSettings (maybe (const True) (==) audience) eJwt <- runExceptT $ do jwt <- decodeCompact payload - verifyClaimsAt validation s time jwt + verifyClaimsAt validation secret time jwt return $ case eJwt of Left e -> JWTInvalid e Right jwt -> JWTClaims $ claims2map jwt jspath diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index a6593214b..e83fd0bb2 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -29,6 +29,7 @@ module PostgREST.Config ( prettyVersion ) where +import Crypto.JWT (JWKSet) import qualified Data.ByteString as B import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as BS @@ -97,7 +98,7 @@ data AppConfig = AppConfig { , configRootSpec :: Maybe Text , configRawMediaTypes :: [B.ByteString] - , configPath :: Maybe FilePath + , configJWKS :: Maybe JWKSet } configPoolTimeout' :: (Fractional a) => AppConfig -> a @@ -230,14 +231,14 @@ readAppConfig cfgPath = do , Handler (\(C.ParseError err) -> exitErr $ "Error parsing config file:\n" <> err) ] - case C.runParser (parseConfig cfgPath) conf of + case C.runParser parseConfig conf of Left err -> exitErr $ "Error parsing config file:\n\t" <> err Right appConf -> return appConf where - parseConfig path = + parseConfig = AppConfig <$> reqString "db-uri" <*> reqString "db-anon-role" @@ -262,7 +263,7 @@ readAppConfig cfgPath = do <*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path") <*> optString "root-spec" <*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types") - <*> pure (Just path) + <*> pure Nothing parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode) parseSocketFileMode k = diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index 98762babe..2c4778c81 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -22,6 +22,7 @@ import Test.Hspec import Test.Hspec.Wai import Text.Heredoc +import PostgREST.Auth (parseSecret) import PostgREST.Config (AppConfig (..)) import PostgREST.Types (JSPathExp (..)) import Protolude hiding (toS) @@ -64,6 +65,7 @@ getEnvVarWithDefault var def = toS <$> _baseCfg :: AppConfig _baseCfg = -- Connection Settings + let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in AppConfig mempty "postgrest_test_anonymous" Nothing (fromList ["test"]) "localhost" 3000 -- No user configured Unix Socket Nothing @@ -74,7 +76,7 @@ _baseCfg = -- Connection Settings -- db-channel-enabled False -- Jwt settings - (Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False Nothing + secret False Nothing -- Connection Modifiers 10 10 Nothing (Just "test.switch_role") -- Debug Settings @@ -90,14 +92,14 @@ _baseCfg = -- Connection Settings Nothing -- Raw output media types [] - -- Config path - Nothing + -- Config JWK + (parseSecret <$> secret) testCfg :: Text -> AppConfig testCfg testDbConn = _baseCfg { configDbUri = testDbConn } testCfgNoJWT :: Text -> AppConfig -testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing } +testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing, configJWKS = Nothing } testUnicodeCfg :: Text -> AppConfig testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchemas = fromList ["تست"] } @@ -109,28 +111,36 @@ testProxyCfg :: Text -> AppConfig testProxyCfg testDbConn = (testCfg testDbConn) { configOpenAPIProxyUri = Just "https://postgrest.com/openapi.json" } testCfgBinaryJWT :: Text -> AppConfig -testCfgBinaryJWT testDbConn = (testCfg testDbConn) { - configJwtSecret = Just . B64.decodeLenient $ - "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" +testCfgBinaryJWT testDbConn = + let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in + (testCfg testDbConn) { + configJwtSecret = secret + , configJWKS = parseSecret <$> secret } testCfgAudienceJWT :: Text -> AppConfig -testCfgAudienceJWT testDbConn = (testCfg testDbConn) { - configJwtSecret = Just . B64.decodeLenient $ - "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=", - configJwtAudience = Just "youraudience" +testCfgAudienceJWT testDbConn = + let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in + (testCfg testDbConn) { + configJwtSecret = secret + , configJwtAudience = Just "youraudience" + , configJWKS = parseSecret <$> secret } testCfgAsymJWK :: Text -> AppConfig -testCfgAsymJWK testDbConn = (testCfg testDbConn) { - configJwtSecret = Just $ encodeUtf8 - [str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|] +testCfgAsymJWK testDbConn = + let secret = Just $ encodeUtf8 [str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|] + in (testCfg testDbConn) { + configJwtSecret = secret + , configJWKS = parseSecret <$> secret } testCfgAsymJWKSet :: Text -> AppConfig -testCfgAsymJWKSet testDbConn = (testCfg testDbConn) { - configJwtSecret = Just $ encodeUtf8 - [str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|] +testCfgAsymJWKSet testDbConn = + let secret = Just $ encodeUtf8 [str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|] + in (testCfg testDbConn) { + configJwtSecret = secret + , configJWKS = parseSecret <$> secret } testNonexistentSchemaCfg :: Text -> AppConfig