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*
tags
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
- Ability to order by jsonb keys - @steve-chavez
- Ability to specify offset for a deeper level - @ruslantalpa
- Ability to use binary base64 encoded secrets - @TrevorBasinger
### Fixed
- Do not apply limit to parent items - @ruslantalpa
+28 -8
View File
@@ -14,9 +14,11 @@ import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.DbStructure
import Control.AutoUpdate
import Data.ByteString.Base64 (decode)
import Data.String (IsString (..))
import Data.Text (stripPrefix)
import Data.Text.IO (hPutStrLn)
import Data.Text (stripPrefix, pack, replace)
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import Data.Text.IO (hPutStrLn, readFile)
import Data.Function (id)
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Hasql.Query as H
@@ -99,9 +101,27 @@ main = do
runSettings appSettings $ postgrest conf refDbStructure pool getTime
loadSecretFile :: AppConfig -> IO AppConfig
loadSecretFile conf = do
let s = configJwtSecret conf
real <- case join (stripPrefix "@" <$> s) of
Nothing -> return s -- the string is the secret, not a filename
Just filename -> sequence . Just $ readFile (toS filename)
return conf { configJwtSecret = real }
loadSecretFile conf = extractAndTransform mSecret
where
mSecret = decodeUtf8 <$> configJwtSecret conf
isB64 = configJwtSecretIsBase64 conf
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
, time
, warp
, bytestring
, base64-bytestring
if !os(windows)
build-depends: unix
@@ -108,6 +110,7 @@ Test-Suite spec
Hs-Source-Dirs: test
Main-Is: Main.hs
Other-Modules: Feature.AuthSpec
, Feature.BinaryJwtSecretSpec
, Feature.ConcurrentSpec
, Feature.CorsSpec
, Feature.DeleteSpec
@@ -126,8 +129,8 @@ Test-Suite spec
, async
, auto-update
, base
, base64-string
, bytestring
, base64-bytestring
, case-insensitive
, cassava
, contravariant
+2 -2
View File
@@ -29,7 +29,7 @@ import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (renderSimpleQuery)
import Network.Wai
import Network.Wai.Middleware.RequestLogger (logStdout)
import Web.JWT (secret)
import Web.JWT (binarySecret)
import Data.Aeson
import Data.Aeson.Types (emptyArray)
@@ -83,7 +83,7 @@ postgrest conf refDbStructure pool getTime =
response <- case userApiRequest (configSchema conf) req body of
Left err -> return $ apiRequestErrResponse err
Right apiRequest -> do
let jwtSecret = secret <$> configJwtSecret conf
let jwtSecret = binarySecret <$> configJwtSecret conf
eClaims = jwtClaims jwtSecret (iJWT apiRequest) time
authed = containsRole eClaims
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
+10 -3
View File
@@ -23,12 +23,14 @@ module PostgREST.Config ( prettyVersion
import System.IO.Error (IOError)
import Control.Applicative
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as C
import Data.List (lookup)
import Data.Text (strip, intercalate, lines)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.IO (hPutStrLn)
import Data.Version (versionBranch)
import Network.Wai
@@ -36,7 +38,7 @@ import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative hiding (str)
import Paths_postgrest (version)
import Text.Heredoc
import Text.PrettyPrint.ANSI.Leijen hiding ((<>))
import Text.PrettyPrint.ANSI.Leijen hiding ((<>), (<$>))
import Protolude hiding (intercalate
, (<>))
@@ -49,7 +51,10 @@ data AppConfig = AppConfig {
, configSchema :: Text
, configHost :: Text
, configPort :: Int
, configJwtSecret :: Maybe Text
, configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool
, configPool :: Int
, configMaxRows :: Maybe Integer
, configReqCheck :: Maybe Text
@@ -105,12 +110,13 @@ readOptions = do
cProxy <- C.lookup conf "server-proxy-uri"
-- jwt ---------------
cJwtSec <- C.lookup conf "jwt-secret"
cJwtB64 <- C.lookupDefault False conf "secret-is-base64"
-- safety ------------
cMaxRows <- C.lookup conf "max-rows"
cReqCheck <- C.lookup conf "pre-request"
return $ AppConfig cDbUri cDbAnon cProxy cDbSchema cHost cPort
cJwtSec cPool cMaxRows cReqCheck False
(encodeUtf8 <$> cJwtSec) cJwtB64 cPool cMaxRows cReqCheck False
where
opts = info (helper <*> pathParser) $
@@ -156,6 +162,7 @@ readOptions = do
|## choose a secret to enable JWT auth
|## (use "@filename" to load from separate file)
|# jwt-secret = "foo"
|# secret-is-base64 = false
|
|## limit rows in response
|# 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
+6
View File
@@ -13,6 +13,7 @@ import Data.IORef
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Feature.AuthSpec
import qualified Feature.BinaryJwtSecretSpec
import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec
@@ -44,6 +45,7 @@ main = do
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime
let reset = resetDb testDbConn
hspec $ do
@@ -65,6 +67,10 @@ main = do
beforeAll_ reset . before noJwtApp $
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
specs = map (uncurry describe) [
("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 System.Environment (getEnv)
import Codec.Binary.Base64.String (encode)
import qualified Data.ByteString.Base64 as B64 (encode, decodeLenient)
import Data.CaseInsensitive (CI(..))
import Data.List (lookup)
import Text.Regex.TDFA ((=~))
@@ -54,25 +54,35 @@ getEnvVarWithDefault var def = do
varValue <- getEnv (toS var) `E.catchIOError` const (return $ toS def)
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 testDbConn =
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 Nothing (Just "test.switch_role") True
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
testCfgNoJWT :: Text -> AppConfig
testCfgNoJWT testDbConn =
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 Nothing 10 Nothing Nothing True
testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing }
testUnicodeCfg :: Text -> AppConfig
testUnicodeCfg testDbConn =
AppConfig testDbConn "postgrest_test_anonymous" Nothing "تست" "localhost" 3000 (Just "safe") 10 Nothing Nothing True
testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchema = "تست" }
testLtdRowsCfg :: Text -> AppConfig
testLtdRowsCfg testDbConn =
AppConfig testDbConn "postgrest_test_anonymous" Nothing "test" "localhost" 3000 (Just "safe") 10 (Just 2) Nothing True
testLtdRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 }
testProxyCfg :: Text -> AppConfig
testProxyCfg testDbConn =
AppConfig testDbConn "postgrest_test_anonymous" (Just "https://postgrest.com/openapi.json") "test" "localhost" 3000 (Just "safe") 10 Nothing Nothing True
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
testCfgBinaryJWT :: Text -> AppConfig
testCfgBinaryJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Just secretBs }
where secretBs = B64.decodeLenient "h2CGB1FoBd51aQooCS2g+UmRgYQfTPQ6v3+9ALbaqM4="
resetDb :: Text -> IO ()
resetDb dbConn = loadFixture dbConn "data"
@@ -99,7 +109,7 @@ matchHeader name valRegex headers =
authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header
authHeaderBasic u p =
(hAuthorization, "Basic " <> (toS . encode . toS $ u <> ":" <> p))
(hAuthorization, "Basic " <> (toS . B64.encode . toS $ u <> ":" <> p))
authHeaderJWT :: BS.ByteString -> Header
authHeaderJWT token =