Skip to content

Commit

Permalink
Support binary (b64) JWT secrets (#772)
Browse files Browse the repository at this point in the history
  • Loading branch information
TrevorBasinger authored and begriffs committed Dec 11, 2016
1 parent 649a841 commit e364cbc
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 41 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -9,3 +9,6 @@ codex.tags
.stack-work*
tags
site
*~
*#*
.#*
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
36 changes: 28 additions & 8 deletions main/Main.hs
Expand Up @@ -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
Expand Down Expand Up @@ -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 "." "="

5 changes: 4 additions & 1 deletion postgrest.cabal
Expand Up @@ -38,6 +38,8 @@ executable postgrest
, text
, time
, warp
, bytestring
, base64-bytestring
if !os(windows)
build-depends: unix

Expand Down Expand Up @@ -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
Expand All @@ -126,8 +129,8 @@ Test-Suite spec
, async
, auto-update
, base
, base64-string
, bytestring
, base64-bytestring
, case-insensitive
, cassava
, contravariant
Expand Down
4 changes: 2 additions & 2 deletions src/PostgREST/App.hs
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
33 changes: 20 additions & 13 deletions src/PostgREST/Config.hs
Expand Up @@ -23,37 +23,42 @@ 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
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
, (<>))

-- | Config file settings for the server
data AppConfig = AppConfig {
configDatabase :: Text
, configAnonRole :: Text
, configProxyUri :: Maybe Text
, configSchema :: Text
, configHost :: Text
, configPort :: Int
, configJwtSecret :: Maybe Text
, configPool :: Int
, configMaxRows :: Maybe Integer
, configReqCheck :: Maybe Text
, configQuiet :: Bool
configDatabase :: Text
, configAnonRole :: Text
, configProxyUri :: Maybe Text
, configSchema :: Text
, configHost :: Text
, configPort :: Int

, configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool

, configPool :: Int
, configMaxRows :: Maybe Integer
, configReqCheck :: Maybe Text
, configQuiet :: Bool
}

defaultCorsPolicy :: CorsResourcePolicy
Expand Down Expand Up @@ -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) $
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions test/Feature/BinaryJwtSecretSpec.hs
@@ -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
16 changes: 11 additions & 5 deletions test/Main.hs
Expand Up @@ -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
Expand All @@ -39,11 +40,12 @@ main = do

result <- P.use pool $ getDbStructure "test"
refDbStructure <- newIORef $ either (panic.show) id result
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime
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
Expand All @@ -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)
Expand Down
34 changes: 22 additions & 12 deletions test/SpecHelper.hs
Expand Up @@ -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 ((=~))
Expand Down Expand Up @@ -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"
Expand All @@ -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 =
Expand Down

0 comments on commit e364cbc

Please sign in to comment.