Allow configurable audience claim (#975)
This commit is contained in:
committed by
Joe Nelson
parent
4ba27d84a4
commit
3ccae4bb8b
@@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #933, OpenAPI externals docs url to current version - @steve-chavez
|
- #933, OpenAPI externals docs url to current version - @steve-chavez
|
||||||
- #962, OpenAPI don't err on nonexistent schema - @steve-chavez
|
- #962, OpenAPI don't err on nonexistent schema - @steve-chavez
|
||||||
- #954, make OpenAPI rpc output dependent on user privileges - @steve-chavez
|
- #954, make OpenAPI rpc output dependent on user privileges - @steve-chavez
|
||||||
|
- #955, Support configurable aud claim - @statik
|
||||||
|
|
||||||
## [0.4.3.0] - 2017-09-06
|
## [0.4.3.0] - 2017-09-06
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
"required": false,
|
"required": false,
|
||||||
"value": "false"
|
"value": "false"
|
||||||
},
|
},
|
||||||
|
"JWT_AUD": {
|
||||||
|
"description": "The audience that should be validated if the JWT token contains an aud claim",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
"MAX_ROWS": {
|
"MAX_ROWS": {
|
||||||
"description": "A hard limit to the number of rows PostgREST will fetch from a view, table, or stored procedure",
|
"description": "A hard limit to the number of rows PostgREST will fetch from a view, table, or stored procedure",
|
||||||
"required": false
|
"required": false
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ ENV PGRST_DB_URI= \
|
|||||||
PGRST_SERVER_PROXY_URI= \
|
PGRST_SERVER_PROXY_URI= \
|
||||||
PGRST_JWT_SECRET= \
|
PGRST_JWT_SECRET= \
|
||||||
PGRST_SECRET_IS_BASE64=false \
|
PGRST_SECRET_IS_BASE64=false \
|
||||||
|
PGRST_JWT_AUD= \
|
||||||
PGRST_MAX_ROWS= \
|
PGRST_MAX_ROWS= \
|
||||||
PGRST_PRE_REQUEST=
|
PGRST_PRE_REQUEST=
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ server-port = "$(PGRST_SERVER_PORT)"
|
|||||||
server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)"
|
server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)"
|
||||||
jwt-secret = "$(PGRST_JWT_SECRET)"
|
jwt-secret = "$(PGRST_JWT_SECRET)"
|
||||||
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
|
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
|
||||||
|
jwt-aud = "$(PGRST_JWT_AUD)"
|
||||||
|
|
||||||
max-rows = "$(PGRST_MAX_ROWS)"
|
max-rows = "$(PGRST_MAX_ROWS)"
|
||||||
pre-request = "$(PGRST_PRE_REQUEST)"
|
pre-request = "$(PGRST_PRE_REQUEST)"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ postgrest conf refDbStructure pool worker =
|
|||||||
response <- case userApiRequest (configSchema conf) req body of
|
response <- case userApiRequest (configSchema conf) req body of
|
||||||
Left err -> return $ apiRequestError err
|
Left err -> return $ apiRequestError err
|
||||||
Right apiRequest -> do
|
Right apiRequest -> do
|
||||||
eClaims <- jwtClaims jwtSecret (toS $ iJWT apiRequest)
|
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest)
|
||||||
|
|
||||||
let authed = containsRole eClaims
|
let authed = containsRole eClaims
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
||||||
|
|||||||
@@ -39,16 +39,16 @@ data JWTAttempt = JWTInvalid JWTError
|
|||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Receives the JWT secret (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 -> BL.ByteString -> IO JWTAttempt
|
jwtClaims :: Maybe JWK -> Text -> BL.ByteString -> IO JWTAttempt
|
||||||
jwtClaims _ "" = return $ JWTClaims M.empty
|
jwtClaims _ "" "" = return $ JWTClaims M.empty
|
||||||
jwtClaims secret payload =
|
jwtClaims secret audience payload =
|
||||||
case secret of
|
case secret of
|
||||||
Nothing -> return JWTMissingSecret
|
Nothing -> return JWTMissingSecret
|
||||||
Just jwk -> do
|
Just jwk -> do
|
||||||
let validation = defaultJWTValidationSettings
|
let validation = set audiencePredicate (== fromString audience) defaultJWTValidationSettings
|
||||||
eJwt <- runExceptT $ do
|
eJwt <- runExceptT $ do
|
||||||
jwt <- decodeCompact payload
|
jwt <- decodeCompact payload
|
||||||
validateJWSJWT validation jwk jwt
|
validateJWSJWT validation jwk jwt
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ data AppConfig = AppConfig {
|
|||||||
|
|
||||||
, configJwtSecret :: Maybe B.ByteString
|
, configJwtSecret :: Maybe B.ByteString
|
||||||
, configJwtSecretIsBase64 :: Bool
|
, configJwtSecretIsBase64 :: Bool
|
||||||
|
, configJwtAudience :: Text
|
||||||
|
|
||||||
, configPool :: Int
|
, configPool :: Int
|
||||||
, configMaxRows :: Maybe Integer
|
, configMaxRows :: Maybe Integer
|
||||||
@@ -117,6 +118,7 @@ readOptions = do
|
|||||||
<*> (fromMaybe 3000 . join . fmap coerceInt <$> C.key "server-port")
|
<*> (fromMaybe 3000 . join . fmap coerceInt <$> C.key "server-port")
|
||||||
<*> (fmap encodeUtf8 . mfilter (/= "") <$> C.key "jwt-secret")
|
<*> (fmap encodeUtf8 . mfilter (/= "") <$> C.key "jwt-secret")
|
||||||
<*> (fromMaybe False . join . fmap coerceBool <$> C.key "secret-is-base64")
|
<*> (fromMaybe False . join . fmap coerceBool <$> C.key "secret-is-base64")
|
||||||
|
<*> (fromMaybe "" <$> C.key "jwt-aud")
|
||||||
<*> (fromMaybe 10 . join . fmap coerceInt <$> C.key "db-pool")
|
<*> (fromMaybe 10 . join . fmap coerceInt <$> C.key "db-pool")
|
||||||
<*> (join . fmap coerceInt <$> C.key "max-rows")
|
<*> (join . fmap coerceInt <$> C.key "max-rows")
|
||||||
<*> (mfilter (/= "") <$> C.key "pre-request")
|
<*> (mfilter (/= "") <$> C.key "pre-request")
|
||||||
@@ -177,6 +179,7 @@ readOptions = do
|
|||||||
|## (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
|
||||||
|
|# jwt-aud = "your_audience_claim"
|
||||||
|
|
|
|
||||||
|## limit rows in response
|
|## limit rows in response
|
||||||
|# max-rows = 1000
|
|# max-rows = 1000
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
module Feature.AudienceJwtSecretSpec 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 "test handling of aud claims in JWT" $ do
|
||||||
|
|
||||||
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
|
it "succeeds with jwt token containing with an audience claim" $ do
|
||||||
|
{- This is the decoded contents of authHeaderJWT
|
||||||
|
|
||||||
|
{
|
||||||
|
"exp": 9999999999,
|
||||||
|
"role": "postgrest_test_author",
|
||||||
|
"id": "jdoe",
|
||||||
|
"aud": "youraudience"
|
||||||
|
}
|
||||||
|
|
||||||
|
-}
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UiLCJhdWQiOiJ5b3VyYXVkaWVuY2UifQ.fJ4tLKSmolWGWehWN20qiU9dMO-WY0RI2VvacL7-ZGo"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
|
it "succeeds with jwt token that does not contain an audience claim" $ do
|
||||||
|
{- This is the decoded contents of authHeaderJWT
|
||||||
|
|
||||||
|
{
|
||||||
|
"exp": 9999999999,
|
||||||
|
"role": "postgrest_test_author",
|
||||||
|
"id": "jdoe"
|
||||||
|
}
|
||||||
|
-}
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` 200
|
||||||
+14
-8
@@ -13,6 +13,7 @@ import Data.IORef
|
|||||||
import qualified Feature.AuthSpec
|
import qualified Feature.AuthSpec
|
||||||
import qualified Feature.AsymmetricJwtSpec
|
import qualified Feature.AsymmetricJwtSpec
|
||||||
import qualified Feature.BinaryJwtSecretSpec
|
import qualified Feature.BinaryJwtSecretSpec
|
||||||
|
import qualified Feature.AudienceJwtSecretSpec
|
||||||
import qualified Feature.ConcurrentSpec
|
import qualified Feature.ConcurrentSpec
|
||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
import qualified Feature.DeleteSpec
|
import qualified Feature.DeleteSpec
|
||||||
@@ -40,14 +41,15 @@ main = do
|
|||||||
|
|
||||||
result <- P.use pool $ getDbStructure "test"
|
result <- P.use pool $ getDbStructure "test"
|
||||||
refDbStructure <- newIORef $ Just $ either (panic.show) id result
|
refDbStructure <- newIORef $ Just $ either (panic.show) id result
|
||||||
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure ()
|
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure ()
|
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure ()
|
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool $ pure ()
|
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool $ pure ()
|
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool $ pure ()
|
||||||
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool $ pure ()
|
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool $ pure ()
|
||||||
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure ()
|
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool $ pure ()
|
||||||
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool $ pure ()
|
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure ()
|
||||||
|
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
|
|
||||||
let reset = resetDb testDbConn
|
let reset = resetDb testDbConn
|
||||||
hspec $ do
|
hspec $ do
|
||||||
@@ -73,6 +75,10 @@ main = do
|
|||||||
beforeAll_ reset . before binaryJwtApp $
|
beforeAll_ reset . before binaryJwtApp $
|
||||||
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
|
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
|
||||||
|
|
||||||
|
-- this test runs with a binary JWT secret and an audience claim
|
||||||
|
beforeAll_ reset . before audJwtApp $
|
||||||
|
describe "Feature.AudienceJwtSecretSpec" Feature.AudienceJwtSecretSpec.spec
|
||||||
|
|
||||||
-- this test runs with asymmetric JWK
|
-- this test runs with asymmetric JWK
|
||||||
beforeAll_ reset . before asymJwkApp $
|
beforeAll_ reset . before asymJwkApp $
|
||||||
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
|
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
|
||||||
|
|||||||
+8
-1
@@ -68,7 +68,7 @@ _baseCfg :: AppConfig
|
|||||||
_baseCfg = -- Connection Settings
|
_baseCfg = -- Connection Settings
|
||||||
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
|
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
|
||||||
-- Jwt settings
|
-- Jwt settings
|
||||||
(Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False
|
(Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False ""
|
||||||
-- Connection Modifiers
|
-- Connection Modifiers
|
||||||
10 Nothing (Just "test.switch_role")
|
10 Nothing (Just "test.switch_role")
|
||||||
-- Debug Settings
|
-- Debug Settings
|
||||||
@@ -95,6 +95,13 @@ testCfgBinaryJWT testDbConn = (testCfg testDbConn) {
|
|||||||
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
|
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testCfgAudienceJWT :: Text -> AppConfig
|
||||||
|
testCfgAudienceJWT testDbConn = (testCfg testDbConn) {
|
||||||
|
configJwtSecret = Just . B64.decodeLenient $
|
||||||
|
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=",
|
||||||
|
configJwtAudience = "youraudience"
|
||||||
|
}
|
||||||
|
|
||||||
testCfgAsymJWK :: Text -> AppConfig
|
testCfgAsymJWK :: Text -> AppConfig
|
||||||
testCfgAsymJWK testDbConn = (testCfg testDbConn) {
|
testCfgAsymJWK testDbConn = (testCfg testDbConn) {
|
||||||
configJwtSecret = Just $ encodeUtf8
|
configJwtSecret = Just $ encodeUtf8
|
||||||
|
|||||||
Reference in New Issue
Block a user