Support binary (b64) JWT secrets (#772)
This commit is contained in:
committed by
Joe Nelson
parent
649a841ef4
commit
e364cbc3ff
@@ -9,3 +9,6 @@ codex.tags
|
|||||||
.stack-work*
|
.stack-work*
|
||||||
tags
|
tags
|
||||||
site
|
site
|
||||||
|
*~
|
||||||
|
*#*
|
||||||
|
.#*
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Custom request validation with `--pre-request` argument - @begriffs
|
- Custom request validation with `--pre-request` argument - @begriffs
|
||||||
- Ability to order by jsonb keys - @steve-chavez
|
- Ability to order by jsonb keys - @steve-chavez
|
||||||
- Ability to specify offset for a deeper level - @ruslantalpa
|
- Ability to specify offset for a deeper level - @ruslantalpa
|
||||||
|
- Ability to use binary base64 encoded secrets - @TrevorBasinger
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Do not apply limit to parent items - @ruslantalpa
|
- Do not apply limit to parent items - @ruslantalpa
|
||||||
|
|||||||
+28
-8
@@ -14,9 +14,11 @@ import PostgREST.OpenAPI (isMalformedProxyUri)
|
|||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
|
|
||||||
import Control.AutoUpdate
|
import Control.AutoUpdate
|
||||||
|
import Data.ByteString.Base64 (decode)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Data.Text (stripPrefix)
|
import Data.Text (stripPrefix, pack, replace)
|
||||||
import Data.Text.IO (hPutStrLn)
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
|
||||||
|
import Data.Text.IO (hPutStrLn, readFile)
|
||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
@@ -99,9 +101,27 @@ main = do
|
|||||||
runSettings appSettings $ postgrest conf refDbStructure pool getTime
|
runSettings appSettings $ postgrest conf refDbStructure pool getTime
|
||||||
|
|
||||||
loadSecretFile :: AppConfig -> IO AppConfig
|
loadSecretFile :: AppConfig -> IO AppConfig
|
||||||
loadSecretFile conf = do
|
loadSecretFile conf = extractAndTransform mSecret
|
||||||
let s = configJwtSecret conf
|
where
|
||||||
real <- case join (stripPrefix "@" <$> s) of
|
mSecret = decodeUtf8 <$> configJwtSecret conf
|
||||||
Nothing -> return s -- the string is the secret, not a filename
|
isB64 = configJwtSecretIsBase64 conf
|
||||||
Just filename -> sequence . Just $ readFile (toS filename)
|
|
||||||
return conf { configJwtSecret = real }
|
extractAndTransform :: Maybe Text -> IO AppConfig
|
||||||
|
extractAndTransform Nothing = return conf
|
||||||
|
extractAndTransform (Just s) =
|
||||||
|
fmap setSecret $ transformString isB64 =<<
|
||||||
|
case stripPrefix "@" s of
|
||||||
|
Nothing -> return s
|
||||||
|
Just filename -> readFile (toS filename)
|
||||||
|
|
||||||
|
transformString :: Bool -> Text -> IO ByteString
|
||||||
|
transformString False t = return . encodeUtf8 $ t
|
||||||
|
transformString True t =
|
||||||
|
case decode (encodeUtf8 $ replaceUrlChars t) of
|
||||||
|
Left errMsg -> panic $ pack errMsg
|
||||||
|
Right bs -> return bs
|
||||||
|
|
||||||
|
setSecret bs = conf { configJwtSecret = Just bs }
|
||||||
|
|
||||||
|
replaceUrlChars = replace "_" "/" . replace "-" "+" . replace "." "="
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -38,6 +38,8 @@ executable postgrest
|
|||||||
, text
|
, text
|
||||||
, time
|
, time
|
||||||
, warp
|
, warp
|
||||||
|
, bytestring
|
||||||
|
, base64-bytestring
|
||||||
if !os(windows)
|
if !os(windows)
|
||||||
build-depends: unix
|
build-depends: unix
|
||||||
|
|
||||||
@@ -108,6 +110,7 @@ Test-Suite spec
|
|||||||
Hs-Source-Dirs: test
|
Hs-Source-Dirs: test
|
||||||
Main-Is: Main.hs
|
Main-Is: Main.hs
|
||||||
Other-Modules: Feature.AuthSpec
|
Other-Modules: Feature.AuthSpec
|
||||||
|
, Feature.BinaryJwtSecretSpec
|
||||||
, Feature.ConcurrentSpec
|
, Feature.ConcurrentSpec
|
||||||
, Feature.CorsSpec
|
, Feature.CorsSpec
|
||||||
, Feature.DeleteSpec
|
, Feature.DeleteSpec
|
||||||
@@ -126,8 +129,8 @@ Test-Suite spec
|
|||||||
, async
|
, async
|
||||||
, auto-update
|
, auto-update
|
||||||
, base
|
, base
|
||||||
, base64-string
|
|
||||||
, bytestring
|
, bytestring
|
||||||
|
, base64-bytestring
|
||||||
, case-insensitive
|
, case-insensitive
|
||||||
, cassava
|
, cassava
|
||||||
, contravariant
|
, contravariant
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import Network.HTTP.Types.Status
|
|||||||
import Network.HTTP.Types.URI (renderSimpleQuery)
|
import Network.HTTP.Types.URI (renderSimpleQuery)
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Network.Wai.Middleware.RequestLogger (logStdout)
|
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||||
import Web.JWT (secret)
|
import Web.JWT (binarySecret)
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Aeson.Types (emptyArray)
|
import Data.Aeson.Types (emptyArray)
|
||||||
@@ -83,7 +83,7 @@ postgrest conf refDbStructure pool getTime =
|
|||||||
response <- case userApiRequest (configSchema conf) req body of
|
response <- case userApiRequest (configSchema conf) req body of
|
||||||
Left err -> return $ apiRequestErrResponse err
|
Left err -> return $ apiRequestErrResponse err
|
||||||
Right apiRequest -> do
|
Right apiRequest -> do
|
||||||
let jwtSecret = secret <$> configJwtSecret conf
|
let jwtSecret = binarySecret <$> configJwtSecret conf
|
||||||
eClaims = jwtClaims jwtSecret (iJWT apiRequest) time
|
eClaims = jwtClaims jwtSecret (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
|
||||||
|
|||||||
+20
-13
@@ -23,12 +23,14 @@ module PostgREST.Config ( prettyVersion
|
|||||||
|
|
||||||
import System.IO.Error (IOError)
|
import System.IO.Error (IOError)
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
|
import qualified Data.ByteString as B
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import qualified Data.Configurator.Types as C
|
import qualified Data.Configurator.Types as C
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.Text (strip, intercalate, lines)
|
import Data.Text (strip, intercalate, lines)
|
||||||
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
import Data.Text.IO (hPutStrLn)
|
import Data.Text.IO (hPutStrLn)
|
||||||
import Data.Version (versionBranch)
|
import Data.Version (versionBranch)
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
@@ -36,24 +38,27 @@ import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
|
|||||||
import Options.Applicative hiding (str)
|
import Options.Applicative hiding (str)
|
||||||
import Paths_postgrest (version)
|
import Paths_postgrest (version)
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
import Text.PrettyPrint.ANSI.Leijen hiding ((<>))
|
import Text.PrettyPrint.ANSI.Leijen hiding ((<>), (<$>))
|
||||||
|
|
||||||
import Protolude hiding (intercalate
|
import Protolude hiding (intercalate
|
||||||
, (<>))
|
, (<>))
|
||||||
|
|
||||||
-- | Config file settings for the server
|
-- | Config file settings for the server
|
||||||
data AppConfig = AppConfig {
|
data AppConfig = AppConfig {
|
||||||
configDatabase :: Text
|
configDatabase :: Text
|
||||||
, configAnonRole :: Text
|
, configAnonRole :: Text
|
||||||
, configProxyUri :: Maybe Text
|
, configProxyUri :: Maybe Text
|
||||||
, configSchema :: Text
|
, configSchema :: Text
|
||||||
, configHost :: Text
|
, configHost :: Text
|
||||||
, configPort :: Int
|
, configPort :: Int
|
||||||
, configJwtSecret :: Maybe Text
|
|
||||||
, configPool :: Int
|
, configJwtSecret :: Maybe B.ByteString
|
||||||
, configMaxRows :: Maybe Integer
|
, configJwtSecretIsBase64 :: Bool
|
||||||
, configReqCheck :: Maybe Text
|
|
||||||
, configQuiet :: Bool
|
, configPool :: Int
|
||||||
|
, configMaxRows :: Maybe Integer
|
||||||
|
, configReqCheck :: Maybe Text
|
||||||
|
, configQuiet :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
@@ -105,12 +110,13 @@ readOptions = do
|
|||||||
cProxy <- C.lookup conf "server-proxy-uri"
|
cProxy <- C.lookup conf "server-proxy-uri"
|
||||||
-- jwt ---------------
|
-- jwt ---------------
|
||||||
cJwtSec <- C.lookup conf "jwt-secret"
|
cJwtSec <- C.lookup conf "jwt-secret"
|
||||||
|
cJwtB64 <- C.lookupDefault False conf "secret-is-base64"
|
||||||
-- safety ------------
|
-- safety ------------
|
||||||
cMaxRows <- C.lookup conf "max-rows"
|
cMaxRows <- C.lookup conf "max-rows"
|
||||||
cReqCheck <- C.lookup conf "pre-request"
|
cReqCheck <- C.lookup conf "pre-request"
|
||||||
|
|
||||||
return $ AppConfig cDbUri cDbAnon cProxy cDbSchema cHost cPort
|
return $ AppConfig cDbUri cDbAnon cProxy cDbSchema cHost cPort
|
||||||
cJwtSec cPool cMaxRows cReqCheck False
|
(encodeUtf8 <$> cJwtSec) cJwtB64 cPool cMaxRows cReqCheck False
|
||||||
|
|
||||||
where
|
where
|
||||||
opts = info (helper <*> pathParser) $
|
opts = info (helper <*> pathParser) $
|
||||||
@@ -156,6 +162,7 @@ readOptions = do
|
|||||||
|## choose a secret to enable JWT auth
|
|## choose a secret to enable JWT auth
|
||||||
|## (use "@filename" to load from separate file)
|
|## (use "@filename" to load from separate file)
|
||||||
|# jwt-secret = "foo"
|
|# jwt-secret = "foo"
|
||||||
|
|# secret-is-base64 = false
|
||||||
|
|
|
|
||||||
|## limit rows in response
|
|## limit rows in response
|
||||||
|# max-rows = 1000
|
|# max-rows = 1000
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module Feature.BinaryJwtSecretSpec where
|
||||||
|
|
||||||
|
-- {{{ Imports
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
-- }}}
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = describe "server started with binary JWT secret" $
|
||||||
|
|
||||||
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
|
it "succeeds with jwt token encoded with a binary secret" $ do
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.l_EcSRWeNtL4OKUTIplrHyioNrff9Rd0MV7RXNCxCyk"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` 200
|
||||||
+11
-5
@@ -13,6 +13,7 @@ import Data.IORef
|
|||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
|
|
||||||
import qualified Feature.AuthSpec
|
import qualified Feature.AuthSpec
|
||||||
|
import qualified Feature.BinaryJwtSecretSpec
|
||||||
import qualified Feature.ConcurrentSpec
|
import qualified Feature.ConcurrentSpec
|
||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
import qualified Feature.DeleteSpec
|
import qualified Feature.DeleteSpec
|
||||||
@@ -39,11 +40,12 @@ main = do
|
|||||||
|
|
||||||
result <- P.use pool $ getDbStructure "test"
|
result <- P.use pool $ getDbStructure "test"
|
||||||
refDbStructure <- newIORef $ either (panic.show) id result
|
refDbStructure <- newIORef $ either (panic.show) id result
|
||||||
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime
|
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime
|
||||||
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime
|
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime
|
||||||
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime
|
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime
|
||||||
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime
|
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime
|
||||||
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime
|
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime
|
||||||
|
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime
|
||||||
|
|
||||||
let reset = resetDb testDbConn
|
let reset = resetDb testDbConn
|
||||||
hspec $ do
|
hspec $ do
|
||||||
@@ -65,6 +67,10 @@ main = do
|
|||||||
beforeAll_ reset . before noJwtApp $
|
beforeAll_ reset . before noJwtApp $
|
||||||
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
|
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
|
||||||
|
|
||||||
|
-- this test runs with a binary JWT secret
|
||||||
|
beforeAll_ reset . before binaryJwtApp $
|
||||||
|
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
specs = map (uncurry describe) [
|
specs = map (uncurry describe) [
|
||||||
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
||||||
|
|||||||
+22
-12
@@ -5,7 +5,7 @@ import Control.Monad (void)
|
|||||||
import qualified System.IO.Error as E
|
import qualified System.IO.Error as E
|
||||||
import System.Environment (getEnv)
|
import System.Environment (getEnv)
|
||||||
|
|
||||||
import Codec.Binary.Base64.String (encode)
|
import qualified Data.ByteString.Base64 as B64 (encode, decodeLenient)
|
||||||
import Data.CaseInsensitive (CI(..))
|
import Data.CaseInsensitive (CI(..))
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
@@ -54,25 +54,35 @@ getEnvVarWithDefault var def = do
|
|||||||
varValue <- getEnv (toS var) `E.catchIOError` const (return $ toS def)
|
varValue <- getEnv (toS var) `E.catchIOError` const (return $ toS def)
|
||||||
return $ toS varValue
|
return $ toS varValue
|
||||||
|
|
||||||
|
_baseCfg :: AppConfig
|
||||||
|
_baseCfg = -- Connection Settings
|
||||||
|
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
|
||||||
|
-- Jwt settings
|
||||||
|
(Just $ encodeUtf8 "safe") False
|
||||||
|
-- Connection Modifiers
|
||||||
|
10 Nothing (Just "test.switch_role")
|
||||||
|
-- Debug Settings
|
||||||
|
True
|
||||||
|
|
||||||
testCfg :: Text -> AppConfig
|
testCfg :: Text -> AppConfig
|
||||||
testCfg testDbConn =
|
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 Nothing (Just "test.switch_role") True
|
|
||||||
|
|
||||||
testCfgNoJWT :: Text -> AppConfig
|
testCfgNoJWT :: Text -> AppConfig
|
||||||
testCfgNoJWT testDbConn =
|
testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing }
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 Nothing 10 Nothing Nothing True
|
|
||||||
|
|
||||||
testUnicodeCfg :: Text -> AppConfig
|
testUnicodeCfg :: Text -> AppConfig
|
||||||
testUnicodeCfg testDbConn =
|
testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchema = "تست" }
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "تست" "localhost" 3000 (Just "safe") 10 Nothing Nothing True
|
|
||||||
|
|
||||||
testLtdRowsCfg :: Text -> AppConfig
|
testLtdRowsCfg :: Text -> AppConfig
|
||||||
testLtdRowsCfg testDbConn =
|
testLtdRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 }
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 (Just 2) Nothing True
|
|
||||||
|
|
||||||
testProxyCfg :: Text -> AppConfig
|
testProxyCfg :: Text -> AppConfig
|
||||||
testProxyCfg testDbConn =
|
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
|
||||||
AppConfig testDbConn "postgrest_test_anonymous" (Just "https://postgrest.com/openapi.json") "test" "localhost" 3000 (Just "safe") 10 Nothing Nothing True
|
|
||||||
|
testCfgBinaryJWT :: Text -> AppConfig
|
||||||
|
testCfgBinaryJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Just secretBs }
|
||||||
|
where secretBs = B64.decodeLenient "h2CGB1FoBd51aQooCS2g+UmRgYQfTPQ6v3+9ALbaqM4="
|
||||||
|
|
||||||
|
|
||||||
resetDb :: Text -> IO ()
|
resetDb :: Text -> IO ()
|
||||||
resetDb dbConn = loadFixture dbConn "data"
|
resetDb dbConn = loadFixture dbConn "data"
|
||||||
@@ -99,7 +109,7 @@ matchHeader name valRegex headers =
|
|||||||
|
|
||||||
authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header
|
authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header
|
||||||
authHeaderBasic u p =
|
authHeaderBasic u p =
|
||||||
(hAuthorization, "Basic " <> (toS . encode . toS $ u <> ":" <> p))
|
(hAuthorization, "Basic " <> (toS . B64.encode . toS $ u <> ":" <> p))
|
||||||
|
|
||||||
authHeaderJWT :: BS.ByteString -> Header
|
authHeaderJWT :: BS.ByteString -> Header
|
||||||
authHeaderJWT token =
|
authHeaderJWT token =
|
||||||
|
|||||||
Reference in New Issue
Block a user