Support binary (b64) JWT secrets (#772)

This commit is contained in:
Trevor Basinger
2016-12-11 09:01:03 -08:00
committed by Joe Nelson
parent 649a841ef4
commit e364cbc3ff
9 changed files with 112 additions and 41 deletions
+3
View File
@@ -9,3 +9,6 @@ codex.tags
.stack-work* .stack-work*
tags tags
site site
*~
*#*
.#*
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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 =