Allow configurable audience claim (#975)

This commit is contained in:
Elliot Murphy
2017-09-26 07:29:20 -07:00
committed by Joe Nelson
parent 4ba27d84a4
commit 3ccae4bb8b
10 changed files with 82 additions and 15 deletions
+1
View File
@@ -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
+4
View File
@@ -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
+1
View File
@@ -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=
+1
View File
@@ -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)"
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
+3
View File
@@ -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
+44
View File
@@ -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
View File
@@ -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
View File
@@ -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