Add support for parsing JSON Web Key Sets
This commit is contained in:
committed by
Steve Chávez
parent
dadfe965b9
commit
473ac70789
@@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- #1205, Add support for parsing JSON Web Key Sets -@russelldavies
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..)
|
|||||||
, mutuallyAgreeable
|
, mutuallyAgreeable
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
)
|
)
|
||||||
import PostgREST.Auth (jwtClaims, containsRole, parseJWK)
|
import PostgREST.Auth (jwtClaims, containsRole, parseSecret)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
import PostgREST.DbRequestBuilder( readRequest
|
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 :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
|
||||||
postgrest conf refDbStructure pool getTime worker =
|
postgrest conf refDbStructure pool getTime worker =
|
||||||
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
|
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
|
||||||
jwtSecret = parseJWK <$> configJwtSecret conf in
|
jwtSecret = parseSecret <$> configJwtSecret conf in
|
||||||
|
|
||||||
middle $ \ req respond -> do
|
middle $ \ req respond -> do
|
||||||
time <- getTime
|
time <- getTime
|
||||||
|
|||||||
+20
-10
@@ -16,7 +16,7 @@ module PostgREST.Auth (
|
|||||||
containsRole
|
containsRole
|
||||||
, jwtClaims
|
, jwtClaims
|
||||||
, JWTAttempt(..)
|
, JWTAttempt(..)
|
||||||
, parseJWK
|
, parseSecret
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Lens.Operators
|
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
|
Receives the JWT secret and audience (from config) and a JWT and returns a map
|
||||||
of JWT claims.
|
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 _ _ "" _ _ = return $ JWTClaims M.empty
|
||||||
jwtClaims secret audience payload time jspath =
|
jwtClaims secret audience payload time jspath =
|
||||||
case secret of
|
case secret of
|
||||||
@@ -83,17 +83,27 @@ containsRole :: JWTAttempt -> Bool
|
|||||||
containsRole (JWTClaims claims) = M.member "role" claims
|
containsRole (JWTClaims claims) = M.member "role" claims
|
||||||
containsRole _ = False
|
containsRole _ = False
|
||||||
|
|
||||||
parseJWK :: ByteString -> JWK
|
{-|
|
||||||
parseJWK str =
|
Parse `jwt-secret` configuration option and turn into a JWKSet.
|
||||||
fromMaybe (hs256jwk str) (JSON.decode (toS str) :: Maybe JWK)
|
|
||||||
|
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
|
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
|
||||||
config file is a simple string rather than a JWK object, we'll
|
|
||||||
apply this function to it.
|
|
||||||
-}
|
-}
|
||||||
hs256jwk :: ByteString -> JWK
|
jwkFromSecret :: ByteString -> JWK
|
||||||
hs256jwk key =
|
jwkFromSecret key =
|
||||||
fromKeyMaterial km
|
fromKeyMaterial km
|
||||||
& jwkUse ?~ Sig
|
& jwkUse ?~ Sig
|
||||||
& jwkAlg ?~ JWSAlg HS256
|
& jwkAlg ?~ JWSAlg HS256
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ readOptions = do
|
|||||||
|## base url for swagger output
|
|## base url for swagger output
|
||||||
|# server-proxy-uri = ""
|
|# 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)
|
|## (use "@filename" to load from separate file)
|
||||||
|# jwt-secret = "foo"
|
|# jwt-secret = "foo"
|
||||||
|# secret-is-base64 = false
|
|# secret-is-base64 = false
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ main = do
|
|||||||
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
|
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||||
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
|
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||||
asymJwkApp = return $ postgrest (testCfgAsymJWK 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 ()
|
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||||
|
|
||||||
let reset :: IO ()
|
let reset :: IO ()
|
||||||
@@ -123,6 +124,10 @@ main = do
|
|||||||
beforeAll_ reset . before asymJwkApp $
|
beforeAll_ reset . before asymJwkApp $
|
||||||
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
|
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
|
-- this test runs with a nonexistent db-schema
|
||||||
beforeAll_ reset . before nonexistentSchemaApp $
|
beforeAll_ reset . before nonexistentSchemaApp $
|
||||||
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
|
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
|
||||||
|
|||||||
@@ -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"}|]
|
[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 :: Text -> AppConfig
|
||||||
testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" }
|
testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user