From 473ac7078941d70e231205b3b70e9f999b5b66da Mon Sep 17 00:00:00 2001 From: Russell Davies Date: Wed, 31 Oct 2018 21:29:46 +0000 Subject: [PATCH] Add support for parsing JSON Web Key Sets --- CHANGELOG.md | 1 + src/PostgREST/App.hs | 4 ++-- src/PostgREST/Auth.hs | 30 ++++++++++++++++++++---------- src/PostgREST/Config.hs | 2 +- test/Main.hs | 5 +++++ test/SpecHelper.hs | 6 ++++++ 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6227d17d..f3f5486db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added +- #1205, Add support for parsing JSON Web Key Sets -@russelldavies ### Fixed diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 477a4a449..99ecbfc97 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -35,7 +35,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..) , mutuallyAgreeable , userApiRequest ) -import PostgREST.Auth (jwtClaims, containsRole, parseJWK) +import PostgREST.Auth (jwtClaims, containsRole, parseSecret) import PostgREST.Config (AppConfig (..)) import PostgREST.DbStructure import PostgREST.DbRequestBuilder( readRequest @@ -65,7 +65,7 @@ import Protolude hiding (intercalate, Proxy) 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 = parseJWK <$> configJwtSecret conf in + jwtSecret = parseSecret <$> configJwtSecret conf in middle $ \ req respond -> do time <- getTime diff --git a/src/PostgREST/Auth.hs b/src/PostgREST/Auth.hs index 2e359a94c..d11dbb936 100644 --- a/src/PostgREST/Auth.hs +++ b/src/PostgREST/Auth.hs @@ -16,7 +16,7 @@ module PostgREST.Auth ( containsRole , jwtClaims , JWTAttempt(..) - , parseJWK + , parseSecret ) where import Control.Lens.Operators @@ -43,7 +43,7 @@ data JWTAttempt = JWTInvalid JWTError Receives the JWT secret and audience (from config) and a JWT and returns a map of JWT claims. -} -jwtClaims :: Maybe JWK -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt +jwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt jwtClaims _ _ "" _ _ = return $ JWTClaims M.empty jwtClaims secret audience payload time jspath = case secret of @@ -83,17 +83,27 @@ containsRole :: JWTAttempt -> Bool containsRole (JWTClaims claims) = M.member "role" claims containsRole _ = False -parseJWK :: ByteString -> JWK -parseJWK str = - fromMaybe (hs256jwk str) (JSON.decode (toS str) :: Maybe JWK) +{-| + Parse `jwt-secret` configuration option and turn into a JWKSet. + + There are three ways to specify `jwt-secret`: text secret, JSON Web Key + (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet + with one key and the last is converted as is. +-} +parseSecret :: ByteString -> JWKSet +parseSecret str = + fromMaybe (maybe secret (\jwk' -> JWKSet [jwk']) maybeJWK) + maybeJWKSet + where + maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet + maybeJWK = JSON.decode (toS str) :: Maybe JWK + secret = JWKSet [jwkFromSecret str] {-| - Internal helper to generate HMAC-SHA256. When the jwt key in the - config file is a simple string rather than a JWK object, we'll - apply this function to it. + Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret. -} -hs256jwk :: ByteString -> JWK -hs256jwk key = +jwkFromSecret :: ByteString -> JWK +jwkFromSecret key = fromKeyMaterial km & jwkUse ?~ Sig & jwkAlg ?~ JWSAlg HS256 diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 621a4fe6e..03681435d 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -209,7 +209,7 @@ readOptions = do |## base url for swagger output |# server-proxy-uri = "" | - |## choose a secret to enable JWT auth + |## choose a secret, JSON Web Key (or set) to enable JWT auth |## (use "@filename" to load from separate file) |# jwt-secret = "foo" |# secret-is-base64 = false diff --git a/test/Main.hs b/test/Main.hs index 086c9928d..5f4ae276b 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -65,6 +65,7 @@ main = do binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure () audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure () asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure () + asymJwkSetApp = return $ postgrest (testCfgAsymJWKSet testDbConn) refDbStructure pool getTime $ pure () nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure () let reset :: IO () @@ -123,6 +124,10 @@ main = do beforeAll_ reset . before asymJwkApp $ describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec + -- this test runs with asymmetric JWKSet + beforeAll_ reset . before asymJwkSetApp $ + describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec + -- this test runs with a nonexistent db-schema beforeAll_ reset . before nonexistentSchemaApp $ describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index 0e5867ca9..53ac02a58 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -113,6 +113,12 @@ testCfgAsymJWK testDbConn = (testCfg testDbConn) { [str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|] } +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"}]}|] + } + testNonexistentSchemaCfg :: Text -> AppConfig testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" }