From bc6c31f13714f3701a474368f1cd350aa399ba92 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 15:41:58 +0530 Subject: [PATCH 01/33] add new optional field `claims_namespace_path` in JWT config --- server/src-lib/Data/Parser/JSONPath.hs | 101 ++++++++++++++--------- server/src-lib/Hasura/App.hs | 6 +- server/src-lib/Hasura/Server/Auth.hs | 2 +- server/src-lib/Hasura/Server/Auth/JWT.hs | 59 +++++++++---- server/src-lib/Hasura/Server/Utils.hs | 16 ++++ 5 files changed, 129 insertions(+), 55 deletions(-) diff --git a/server/src-lib/Data/Parser/JSONPath.hs b/server/src-lib/Data/Parser/JSONPath.hs index fbca6ac476379..efc1c05d44908 100644 --- a/server/src-lib/Data/Parser/JSONPath.hs +++ b/server/src-lib/Data/Parser/JSONPath.hs @@ -2,56 +2,53 @@ module Data.Parser.JSONPath ( parseJSONPath , JSONPathElement(..) , JSONPath + , formatPath ) where import Control.Applicative ((<|>)) import Data.Aeson.Internal (JSONPath, JSONPathElement (..)) import Data.Attoparsec.Text import Data.Bool (bool) -import Data.Char (isDigit) -import qualified Data.Text as T +import Data.Char import Prelude hiding (takeWhile) import Text.Read (readMaybe) +import qualified Data.Text as T -parseKey :: Parser T.Text -parseKey = do - firstChar <- letter - "the first character of property name must be a letter." - name <- many' (letter - <|> digit - <|> satisfy (`elem` ("-_" :: String)) - ) - return $ T.pack (firstChar:name) +parseSimpleKeyText :: Parser T.Text +parseSimpleKeyText = takeWhile1 (inClass "a-zA-Z0-9_-") -parseIndex :: Parser Int -parseIndex = skip (== '[') *> anyChar >>= parseDigits - where - parseDigits :: Char -> Parser Int - parseDigits firstDigit - | firstDigit == ']' = fail "empty array index" - | not $ isDigit firstDigit = - fail $ "invalid array index: " ++ [firstDigit] - | otherwise = do - remain <- many' (notChar ']') - skip (== ']') - let content = firstDigit:remain - case (readMaybe content :: Maybe Int) of - Nothing -> fail $ "invalid array index: " ++ content - Just v -> return v +parseKey :: Parser JSONPathElement +parseKey = Key <$> + ( (char '.' *> parseSimpleKeyText) -- Parse `.key` + <|> T.pack <$> ((string ".['" <|> string "['") *> manyTill anyChar (string "']")) -- Parse `['key']` or `.['key']` + <|> fail "invalid key element" + ) -parseElement :: Parser JSONPathElement -parseElement = do - dotLen <- T.length <$> takeWhile (== '.') - if dotLen > 1 - then fail "multiple dots in json path" - else peekChar >>= \case - Nothing -> fail "empty json path" - Just '[' -> Index <$> parseIndex - _ -> Key <$> parseKey +parseIndex :: Parser JSONPathElement +parseIndex = Index <$> + ( ((char '[' *> manyTill anyChar (char ']')) >>= maybe (fail "invalid array index") pure . readMaybe) -- Parse `[Int]` + <|> fail "invalid index element" + ) parseElements :: Parser JSONPath -parseElements = skipWhile (== '$') *> many1 parseElement +parseElements = skipWhile (== '$') *> parseRemaining + where + parseFirstKey = Key <$> parseSimpleKeyText + parseElements' = many1 (parseIndex <|> parseKey) + parseRemaining = do + maybeFirstChar <- peekChar + case maybeFirstChar of + Nothing -> pure [] + Just firstChar -> + -- If first char is not any of '.' and '[', then parse first key + -- Eg:- Parse "key1.key2[0]" + if firstChar `notElem` (".[" :: String) then do + firstKey <- parseFirstKey + remainingElements <- parseElements' + pure $ firstKey:remainingElements + else parseElements' +-- | Parse jsonpath String value parseJSONPath :: T.Text -> Either String JSONPath parseJSONPath = parseResult . parse parseElements where @@ -64,6 +61,36 @@ parseJSONPath = parseResult . parse parseElements Left $ invalidMessage remain else Right r - invalidMessage s = "invalid property name: " ++ T.unpack s ++ ". Accept letters, digits, underscore (_) or hyphen (-) only" + ++ ". Use single quotes enclosed in bracket if there are any special characters" + +-- available in Data.Aeson.Types but not importing +formatPath :: JSONPath -> String +formatPath path = "$" ++ formatRelativePath path + +formatRelativePath :: JSONPath -> String +formatRelativePath path = format "" path + where + format :: String -> JSONPath -> String + format pfx [] = pfx + format pfx (Index idx:parts) = format (pfx ++ "[" ++ show idx ++ "]") parts + format pfx (Key key:parts) = format (pfx ++ formatKey key) parts + + formatKey :: T.Text -> String + formatKey key + | isIdentifierKey strKey = "." ++ strKey + | otherwise = "['" ++ escapeKey strKey ++ "']" + where strKey = T.unpack key + + isIdentifierKey :: String -> Bool + isIdentifierKey [] = False + isIdentifierKey (x:xs) = isAlpha x && all isAlphaNum xs + + escapeKey :: String -> String + escapeKey = concatMap escapeChar + + escapeChar :: Char -> String + escapeChar '\'' = "\\'" + escapeChar '\\' = "\\\\" + escapeChar c = [c] diff --git a/server/src-lib/Hasura/App.hs b/server/src-lib/Hasura/App.hs index 9ebfe3d4c7df8..81753be97d83e 100644 --- a/server/src-lib/Hasura/App.hs +++ b/server/src-lib/Hasura/App.hs @@ -214,7 +214,7 @@ runHGEServer -- ^ start time -> m () runHGEServer ServeOptions{..} InitCtx{..} initTime = do - -- Comment this to enable expensive assertions from "GHC.AssertNF". These will log lines to + -- Comment this to enable expensive assertions from "GHC.AssertNF". These will log lines to -- STDOUT containing "not in normal form". In the future we could try to integrate this into -- our tests. For now this is a development tool. -- @@ -251,7 +251,7 @@ runHGEServer ServeOptions{..} InitCtx{..} initTime = do liftIO $ logInconsObjs logger inconsObjs -- start background threads for schema sync - (_schemaSyncListenerThread, _schemaSyncProcessorThread) <- + (_schemaSyncListenerThread, _schemaSyncProcessorThread) <- startSchemaSyncThreads sqlGenCtx _icPgPool logger _icHttpManager cacheRef _icInstanceId cacheInitTime @@ -279,7 +279,7 @@ runHGEServer ServeOptions{..} InitCtx{..} initTime = do asyncActionsProcessor (_scrCache cacheRef) _icPgPool _icHttpManager -- start a background thread to check for updates - _updateThread <- C.forkImmortal "checkForUpdates" logger $ liftIO $ + _updateThread <- C.forkImmortal "checkForUpdates" logger $ liftIO $ checkForUpdates loggerCtx _icHttpManager -- start a background thread for telemetry diff --git a/server/src-lib/Hasura/Server/Auth.hs b/server/src-lib/Hasura/Server/Auth.hs index 3d03864d17b1a..2073dbf8cb5ea 100644 --- a/server/src-lib/Hasura/Server/Auth.hs +++ b/server/src-lib/Hasura/Server/Auth.hs @@ -114,7 +114,7 @@ mkJwtCtx JWTConfig{..} httpManager logger = do Left jwk -> liftIO $ newIORef (JWKSet [jwk]) Right url -> getJwkFromUrl url let claimsFmt = fromMaybe JCFJson jcClaimsFormat - return $ JWTCtx jwkRef jcClaimNs jcAudience claimsFmt jcIssuer + return $ JWTCtx jwkRef jcClaimNs jcClaimNsPath jcAudience claimsFmt jcIssuer where -- if we can't find any expiry time for the JWK (either in @Expires@ header or @Cache-Control@ -- header), do not start a background thread for refreshing the JWK diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index ffc75250cb193..fbfe00c671c0f 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -22,6 +22,7 @@ import Data.Time.Clock (NominalDiffTime, UTCTime, diff import GHC.AssertNF import Network.URI (URI) +import Data.Aeson.Internal (JSONPath) import Data.Parser.CacheControl import Data.Parser.Expires import Hasura.HTTP @@ -30,7 +31,7 @@ import Hasura.Prelude import Hasura.RQL.Types import Hasura.Server.Auth.JWT.Internal (parseHmacKey, parseRsaKey) import Hasura.Server.Auth.JWT.Logging -import Hasura.Server.Utils (getRequestHeader, userRoleHeader) +import Hasura.Server.Utils (getRequestHeader, userRoleHeader, executeJSONPath) import Hasura.Server.Version (HasVersion) import qualified Control.Concurrent.Extended as C @@ -38,6 +39,7 @@ import qualified Crypto.JWT as Jose import qualified Data.Aeson as J import qualified Data.Aeson.Casing as J import qualified Data.Aeson.TH as J +import qualified Data.Aeson.Internal as J import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as BLC import qualified Data.CaseInsensitive as CI @@ -47,7 +49,7 @@ import qualified Data.Text.Encoding as T import qualified Network.HTTP.Client as HTTP import qualified Network.HTTP.Types as HTTP import qualified Network.Wreq as Wreq - +import qualified Data.Parser.JSONPath as JSONPath newtype RawJWT = RawJWT BL.ByteString @@ -64,6 +66,7 @@ data JWTConfig { jcType :: !T.Text , jcKeyOrUrl :: !(Either Jose.JWK URI) , jcClaimNs :: !(Maybe T.Text) + , jcClaimNsPath :: !(Maybe JSONPath) , jcAudience :: !(Maybe Jose.Audience) , jcClaimsFormat :: !(Maybe JWTClaimsFormat) , jcIssuer :: !(Maybe Jose.StringOrURI) @@ -73,14 +76,15 @@ data JWTCtx = JWTCtx { jcxKey :: !(IORef Jose.JWKSet) , jcxClaimNs :: !(Maybe T.Text) + , jcxClaimNsPath :: !(Maybe JSONPath) , jcxAudience :: !(Maybe Jose.Audience) , jcxClaimsFormat :: !JWTClaimsFormat , jcxIssuer :: !(Maybe Jose.StringOrURI) } deriving (Eq) instance Show JWTCtx where - show (JWTCtx _ nsM audM cf iss) = - show ["", show nsM, show audM, show cf, show iss] + show (JWTCtx _ nsM nsPath audM cf iss) = + show ["", show nsM,show nsPath,show audM, show cf, show iss] data HasuraClaims = HasuraClaims @@ -237,7 +241,12 @@ processAuthZHeader jwtCtx headers authzHeader = do expTimeM = fmap (\(Jose.NumericDate t) -> t) $ claims ^. Jose.claimExp -- see if the hasura claims key exist in the claims map - let mHasuraClaims = Map.lookup claimsNs $ claims ^. Jose.unregisteredClaims + let mHasuraClaims = + case jcxClaimNsPath jwtCtx of + -- if `claim_namespace_path` is null, then look for hasura claims in the key `claim_namespace` + Nothing -> Map.lookup claimsNs $ claims ^. Jose.unregisteredClaims + Just path -> parseIValueJsonValue $ executeJSONPath path (J.toJSON $ claims ^. Jose.unregisteredClaims) + hasuraClaimsV <- maybe claimsNotFound return mHasuraClaims -- get hasura claims value as an object. parse from string possibly @@ -284,6 +293,9 @@ processAuthZHeader jwtCtx headers authzHeader = do claimsErr = throw400 JWTInvalidClaims + parseIValueJsonValue (J.IError _ _) = Nothing + parseIValueJsonValue (J.ISuccess v) = Just v + -- see if there is a x-hasura-role header, or else pick the default role getCurrentRole defaultRole = let mUserRole = getRequestHeader userRoleHeader headers @@ -369,13 +381,14 @@ verifyJwt ctx (RawJWT rawJWT) = do instance J.ToJSON JWTConfig where - toJSON (JWTConfig ty keyOrUrl claimNs aud claimsFmt iss) = + toJSON (JWTConfig ty keyOrUrl claimNs claimNsPath aud claimsFmt iss) = case keyOrUrl of Left _ -> mkObj ("key" J..= J.String "") Right url -> mkObj ("jwk_url" J..= url) where mkObj item = J.object [ "type" J..= ty , "claims_namespace" J..= claimNs + , "claims_namespace_path" J..= maybe Nothing (Just . JSONPath.formatPath) claimNsPath , "claims_format" J..= claimsFmt , "audience" J..= aud , "issuer" J..= iss @@ -391,19 +404,37 @@ instance J.FromJSON JWTConfig where keyType <- o J..: "type" mRawKey <- o J..:? "key" claimNs <- o J..:? "claims_namespace" + claimNsPathStr <- o J..:? "claims_namespace_path" aud <- o J..:? "audience" iss <- o J..:? "issuer" jwkUrl <- o J..:? "jwk_url" isStrngfd <- o J..:? "claims_format" - case (mRawKey, jwkUrl) of - (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" - (Just _, Just _) -> fail "key, jwk_url both cannot be present" - (Just rawKey, Nothing) -> do - key <- parseKey keyType rawKey - return $ JWTConfig keyType (Left key) claimNs aud isStrngfd iss - (Nothing, Just url) -> - return $ JWTConfig keyType (Right url) claimNs aud isStrngfd iss + case claimNsPathStr of + Just nsPathStr -> + let eClaimNsPath = JSONPath.parseJSONPath nsPathStr + in case eClaimNsPath of + Left err -> fail ("invalid JSON path claims_namespace_path " ++ (show nsPathStr) + ++ " error " ++ (show err)) + Right nsPath -> + case (mRawKey, jwkUrl) of + (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" + (Just _, Just _) -> fail "key, jwk_url both cannot be present" + (Just rawKey, Nothing) -> do + key <- parseKey keyType rawKey + return $ JWTConfig keyType (Left key) claimNs (Just nsPath) aud isStrngfd iss + (Nothing, Just url) -> + return $ JWTConfig keyType (Right url) claimNs (Just nsPath) aud isStrngfd iss + Nothing -> + case (mRawKey, jwkUrl) of + (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" + (Just _, Just _) -> fail "key, jwk_url both cannot be present" + (Just rawKey, Nothing) -> do + key <- parseKey keyType rawKey + return $ JWTConfig keyType (Left key) claimNs Nothing aud isStrngfd iss + (Nothing, Just url) -> + return $ JWTConfig keyType (Right url) claimNs Nothing aud isStrngfd iss + where parseKey keyType rawKey = diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index 72d55e55c1aa9..4d5d74a7cc7a0 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -11,6 +11,7 @@ import Language.Haskell.TH.Syntax (Lift, Q, TExp) import System.Environment import System.Exit import System.Process +import Data.Aeson.Internal import qualified Data.ByteString as B import qualified Data.CaseInsensitive as CI @@ -27,6 +28,7 @@ import qualified Network.Wreq as Wreq import qualified Text.Regex.TDFA as TDFA import qualified Text.Regex.TDFA.ReadRegex as TDFA import qualified Text.Regex.TDFA.TDFA as TDFA +import qualified Data.Vector as V import Hasura.RQL.Instances () @@ -227,3 +229,17 @@ makeReasonMessage errors showError = [singleError] -> "because " <> showError singleError _ -> "for the following reasons:\n" <> T.unlines (map ((" • " <>) . showError) errors) + +executeJSONPath :: JSONPath -> Value -> IResult Value +executeJSONPath jsonPath = iparse (valueParser jsonPath) + where + valueParser path value = case path of + [] -> fail "Empty JSON Path" + [pathElement] -> parseWithPathElement pathElement value + (pathElement:remaining) -> parseWithPathElement pathElement value >>= + (( pathElement) . valueParser remaining) + where + parseWithPathElement = \case + Key k -> withObject "Object" (.: k) + Index i -> withArray "Array" $ + maybe (fail "Array index out of range") pure . (V.!? i) From a5cb5dbeccebef303500c4ea1256f3f87de4cea5 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 18:03:09 +0530 Subject: [PATCH 02/33] modify the tests to include the claims_namespace_path --- .circleci/test-server.sh | 15 +++++++++++++++ server/tests-py/conftest.py | 4 ++++ server/tests-py/test_jwt.py | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh index eabd8cd1b4fcc..64d167bc3f480 100755 --- a/.circleci/test-server.sh +++ b/.circleci/test-server.sh @@ -318,6 +318,21 @@ kill_hge_servers unset HASURA_GRAPHQL_JWT_SECRET +########## +echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_namespace_path) #####################################>\n" +TEST_TYPE="jwt" + +init_jwt + +export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasuraClaims"}')" + +start_multiple_hge_servers + +run_pytest_parallel --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" --hge-jwt-claims-ns-path="$.hasuraClaims" + +kill_hge_servers + +unset HASURA_GRAPHQL_JWT_SECRET # test with CORS modes diff --git a/server/tests-py/conftest.py b/server/tests-py/conftest.py index 7f2e9f189e603..7586250da8ab0 100644 --- a/server/tests-py/conftest.py +++ b/server/tests-py/conftest.py @@ -39,6 +39,10 @@ def pytest_addoption(parser): "--hge-jwt-conf", metavar="HGE_JWT_CONF", help="The JWT conf", required=False ) + parser.addoption( + "--hge-jwt-claims-ns-path", metavar="HGE_JWT_CLAIMS_NS_PATH", help="The JWT conf claims namespace path", required=False + ) + parser.addoption( "--test-cors", action="store_true", required=False, diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index 131f77f4f8b41..1e8753b71aa9e 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -31,6 +31,11 @@ def get_claims_fmt(raw_conf): def mk_claims(conf, claims): claims_fmt = get_claims_fmt(conf) + claims_namespace_path = PytestConf.config.getoption('--hge-jwt-claims-ns-path') + if claims_namespace_path == '$.hasuraClaims': + claims = { + "hasuraClaims":claims + } if claims_fmt == 'json': return claims elif claims_fmt == 'stringified_json': From 2e74586a63cca134356cb7fbe1cb3cc83cb2690b Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 19:50:46 +0530 Subject: [PATCH 03/33] return value when empty array is found in executeJSONPath --- .circleci/test-server.sh | 7 +++---- server/src-lib/Hasura/Server/Utils.hs | 2 +- server/tests-py/test_jwt.py | 15 +++++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh index 64d167bc3f480..e4940823e121a 100755 --- a/.circleci/test-server.sh +++ b/.circleci/test-server.sh @@ -322,13 +322,12 @@ unset HASURA_GRAPHQL_JWT_SECRET echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_namespace_path) #####################################>\n" TEST_TYPE="jwt" -init_jwt +run_hge_with_args serve +wait_for_port 8080 export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasuraClaims"}')" -start_multiple_hge_servers - -run_pytest_parallel --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" --hge-jwt-claims-ns-path="$.hasuraClaims" +pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" --hge-jwt-claims-ns-path="$.hasuraClaims" test_jwt.py kill_hge_servers diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index 4d5d74a7cc7a0..2c01e5a41489d 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -234,7 +234,7 @@ executeJSONPath :: JSONPath -> Value -> IResult Value executeJSONPath jsonPath = iparse (valueParser jsonPath) where valueParser path value = case path of - [] -> fail "Empty JSON Path" + [] -> pure value [pathElement] -> parseWithPathElement pathElement value (pathElement:remaining) -> parseWithPathElement pathElement value >>= (( pathElement) . valueParser remaining) diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index 1e8753b71aa9e..cee681e38ab33 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -31,11 +31,6 @@ def get_claims_fmt(raw_conf): def mk_claims(conf, claims): claims_fmt = get_claims_fmt(conf) - claims_namespace_path = PytestConf.config.getoption('--hge-jwt-claims-ns-path') - if claims_namespace_path == '$.hasuraClaims': - claims = { - "hasuraClaims":claims - } if claims_fmt == 'json': return claims elif claims_fmt == 'stringified_json': @@ -47,16 +42,20 @@ def mk_claims(conf, claims): class TestJWTBasic(): def test_jwt_valid_claims_success(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['user', 'editor'], 'x-hasura-default-role': 'user' }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + elif claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint self.conf['status'] = 200 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False, claims_namespace_path=claims_namespace_path) def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { @@ -80,7 +79,7 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False, claims_namespace_path=claims_namespace_path) def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { From 35cb5fead60410d9e649edbe4667928f4bd63ae3 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 19:54:47 +0530 Subject: [PATCH 04/33] modify the tests to incorporate claims_namespace_path in JWT --- server/tests-py/test_jwt.py | 96 ++++++++++++++++++++++++++++--------- server/tests-py/validate.py | 7 ++- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index cee681e38ab33..f6c037aa2c43f 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -43,26 +43,30 @@ class TestJWTBasic(): def test_jwt_valid_claims_success(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { - 'x-hasura-user-id': '1', - 'x-hasura-allowed-roles': ['user', 'editor'], - 'x-hasura-default-role': 'user' + 'x-hasura-user-id': '1', + 'x-hasura-allowed-roles': ['user', 'editor'], + 'x-hasura-default-role': 'user' }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": + else if claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint self.conf['status'] = 200 - check_query(hge_ctx, self.conf, add_auth=False, claims_namespace_path=claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False) def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['contractor', 'editor'], 'x-hasura-default-role': 'contractor' }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -79,13 +83,17 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False, claims_namespace_path=claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False) def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user' }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -105,11 +113,15 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False) def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': 'user', 'x-hasura-default-role': 'user' }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -129,10 +141,14 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False) def test_jwt_no_default_role(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -152,11 +168,15 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False) def test_jwt_expired(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims exp = datetime.utcnow() - timedelta(minutes=1) self.claims['exp'] = round(exp.timestamp()) @@ -179,12 +199,15 @@ def test_jwt_expired(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False) def test_jwt_invalid_signature(self, hge_ctx, endpoint): - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims wrong_key = gen_rsa_key() token = jwt.encode(self.claims, wrong_key, algorithm='HS256').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -209,12 +232,15 @@ def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): if 'audience' in jwt_conf: pytest.skip('audience present in conf, skipping testing no audience') - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims self.claims['aud'] = 'hasura-test-suite' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -226,12 +252,15 @@ def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): if 'issuer' in jwt_conf: pytest.skip('issuer present in conf, skipping testing no issuer') - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims self.claims['iss'] = 'rubbish-issuer' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -284,11 +313,15 @@ def test_jwt_expiry(self, hge_ctx, ws_client): 'name': 'John Doe', 'iat': math.floor(curr_time.timestamp()) } - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims exp = curr_time + timedelta(seconds=4) self.claims['exp'] = round(exp.timestamp()) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -311,11 +344,15 @@ def test_jwt_valid_audience(self, hge_ctx, endpoint): audience = jwt_conf['audience'] audience = audience if isinstance(audience, str) else audience[0] - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims self.claims['aud'] = audience token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -327,11 +364,16 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): if 'audience' not in jwt_conf: pytest.skip('audience not present in conf, skipping testing audience') - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims + self.claims['aud'] = 'rubbish_audience' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -383,11 +425,16 @@ def test_jwt_valid_issuer(self, hge_ctx, endpoint): pytest.skip('issuer not present in conf, skipping testing issuer') issuer = jwt_conf['issuer'] - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims + self.claims['iss'] = issuer token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -399,11 +446,16 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): if 'issuer' not in jwt_conf: pytest.skip('issuer not present in conf, skipping testing issuer') - self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, { + claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + if claims_namespace_path is None: + self.claims['https://hasura.io/jwt/claims'] = claims + else if claims_namespace_path == "$.hasuraClaims": + self.claims['hasuraClaims'] = claims + self.claims['iss'] = 'rubbish_issuer' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') diff --git a/server/tests-py/validate.py b/server/tests-py/validate.py index 51bfc327841b4..9cb332f26aedd 100644 --- a/server/tests-py/validate.py +++ b/server/tests-py/validate.py @@ -125,7 +125,7 @@ def test_forbidden_webhook(hge_ctx, conf): # Returns the response received and a bool indicating whether the test passed # or not (this will always be True unless we are `--accepting`) -def check_query(hge_ctx, conf, transport='http', add_auth=True): +def check_query(hge_ctx, conf, transport='http', add_auth=True, claims_namespace_path=None): hge_ctx.tests_passed = True headers = {} if 'headers' in conf: @@ -150,8 +150,11 @@ def check_query(hge_ctx, conf, transport='http', add_auth=True): claim = { "sub": "foo", "name": "bar", - "https://hasura.io/jwt/claims": hClaims } + if claims_namespace_path is None: + claim["https://hasura.io/jwt/claims"] = hClaims + elif claims_namespace_path == "$.hasuraClaims": + claim['hasuraClaims'] = hClaims headers['Authorization'] = 'Bearer ' + jwt.encode(claim, hge_ctx.hge_jwt_key, algorithm='RS512').decode( 'UTF-8') From b7412827a98f45f5aedb29af9b2382cd1f84caf1 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 20:22:19 +0530 Subject: [PATCH 05/33] fix the syntax error in the tests --- server/tests-py/test_jwt.py | 56 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index f6c037aa2c43f..cdb0052a0e36f 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -21,6 +21,8 @@ if not PytestConf.config.getoption('--hge-jwt-conf'): pytest.skip('--hge-jwt-key-conf is missing, skipping JWT tests', allow_module_level=True) +claims_namespace_path = PytestConf.config.getoption('--hge-jwt-claims-ns-path') + def get_claims_fmt(raw_conf): conf = json.loads(raw_conf) try: @@ -49,13 +51,13 @@ def test_jwt_valid_claims_success(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint self.conf['status'] = 200 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -65,7 +67,7 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -83,7 +85,7 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -92,7 +94,7 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -110,7 +112,7 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -120,7 +122,7 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -138,7 +140,7 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_no_default_role(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -147,7 +149,7 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -165,7 +167,7 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_expired(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -175,7 +177,7 @@ def test_jwt_expired(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims exp = datetime.utcnow() - timedelta(minutes=1) self.claims['exp'] = round(exp.timestamp()) @@ -196,7 +198,7 @@ def test_jwt_expired(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_invalid_signature(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -206,7 +208,7 @@ def test_jwt_invalid_signature(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims wrong_key = gen_rsa_key() token = jwt.encode(self.claims, wrong_key, algorithm='HS256').decode('utf-8') @@ -225,7 +227,7 @@ def test_jwt_invalid_signature(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -239,13 +241,13 @@ def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims self.claims['aud'] = 'hasura-test-suite' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -259,13 +261,13 @@ def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims self.claims['iss'] = 'rubbish-issuer' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) @pytest.fixture(autouse=True) def transact(self, setup): @@ -320,7 +322,7 @@ def test_jwt_expiry(self, hge_ctx, ws_client): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims exp = curr_time + timedelta(seconds=4) self.claims['exp'] = round(exp.timestamp()) @@ -351,13 +353,13 @@ def test_jwt_valid_audience(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims self.claims['aud'] = audience token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_invalid_audience(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -371,7 +373,7 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims self.claims['aud'] = 'rubbish_audience' @@ -392,7 +394,7 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) @pytest.fixture(autouse=True) def transact(self, setup): @@ -432,14 +434,14 @@ def test_jwt_valid_issuer(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims self.claims['iss'] = issuer token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) def test_jwt_invalid_issuer(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -453,7 +455,7 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): }) if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims - else if claims_namespace_path == "$.hasuraClaims": + elif claims_namespace_path == "$.hasuraClaims": self.claims['hasuraClaims'] = claims self.claims['iss'] = 'rubbish_issuer' @@ -474,7 +476,7 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) @pytest.fixture(autouse=True) def transact(self, setup): From 0ffeb29703de82274a14ca3b775a5a944a05c251 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 20:48:55 +0530 Subject: [PATCH 06/33] update the docs related to claims_namespace_path --- CHANGELOG.md | 1 + .../manual/auth/authentication/jwt.rst | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c47be007b7ae..1684e4cc44ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The order and collapsed state of columns is now persisted across page navigation - server: fix an edge case where some events wouldn't be processed because of internal erorrs (#4213) - server: fix downgrade not working to version v1.1.1 (#4354) - server: `type` field is not required if `jwk_url` is provided in JWT config +- server: add a new field `claims_namespace_path` which accepts a JSON Path for looking up hasura claim in the JWT token (#4349) ## `v1.2.0-beta.3` diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index fbc09c7752ad0..f9298adb1bf74 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -130,6 +130,7 @@ JSON object: "jwk_url": "", "claims_namespace": "", "claims_format": "json|stringified_json", + "claims_namespace_path":"", "audience": , "issuer": "" } @@ -168,6 +169,35 @@ https://tools.ietf.org/html/rfc7517. This is an optional field. You can also provide the key (certificate, PEM encoded public key) as a string - in the ``key`` field along with the ``type``. +``claims_namespace_path`` +^^^^^^^^^^^^^^^^^^^^^^^^^ +An optional JSON path value to the hasura claims in the JWT token (e.g. ``$.hasura.claims``) + +For example, the JWT token should in this format if the ``claims_namespace_path`` is +set to ``$.hasura.claims``: + +.. code-block:: json + + { + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "iat": 1516239022, + "hasura": { + "claims": { + "x-hasura-allowed-roles": ["editor","user", "mod"], + "x-hasura-default-role": "user", + "x-hasura-user-id": "1234567890", + "x-hasura-org-id": "123", + "x-hasura-custom": "custom-value" + } + } + } + + +This is an optional field, if the value is not provided, then the ``claims_namespace`` value +is used to get the name of the claims field among the top-level keys. + Rotating JWKs +++++++++++++ From 733ae613c302f456899d5d04997215a584a6808b Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 21:25:20 +0530 Subject: [PATCH 07/33] fix the keyword argument syntax in test_jwt.py --- server/tests-py/test_jwt.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index cdb0052a0e36f..b1bf5ce9d6528 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -57,7 +57,7 @@ def test_jwt_valid_claims_success(self, hge_ctx, endpoint): self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint self.conf['status'] = 200 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -85,7 +85,7 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -112,7 +112,7 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -140,7 +140,7 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_no_default_role(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -167,7 +167,7 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_expired(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -198,7 +198,7 @@ def test_jwt_expired(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_signature(self, hge_ctx, endpoint): claims = mk_claims(hge_ctx.hge_jwt_conf, { @@ -227,7 +227,7 @@ def test_jwt_invalid_signature(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -247,7 +247,7 @@ def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -267,7 +267,7 @@ def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) @pytest.fixture(autouse=True) def transact(self, setup): @@ -275,7 +275,7 @@ def transact(self, setup): with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c: self.conf = yaml.safe_load(c) curr_time = datetime.utcnow() - exp_time = curr_time + timedelta(hours=1) + exp_time = curr_time + timedelta(hours=10) self.claims = { 'sub': '1234567890', 'name': 'John Doe', @@ -359,7 +359,7 @@ def test_jwt_valid_audience(self, hge_ctx, endpoint): token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_audience(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -394,7 +394,7 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) @pytest.fixture(autouse=True) def transact(self, setup): @@ -441,7 +441,7 @@ def test_jwt_valid_issuer(self, hge_ctx, endpoint): token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_issuer(self, hge_ctx, endpoint): jwt_conf = json.loads(hge_ctx.hge_jwt_conf) @@ -476,7 +476,7 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): self.conf['status'] = 200 if endpoint == '/v1alpha1/graphql': self.conf['status'] = 400 - check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path) + check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) @pytest.fixture(autouse=True) def transact(self, setup): From ababdd30d9e64df08fb616a8f8187b530de062f9 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 10 Apr 2020 22:37:58 +0530 Subject: [PATCH 08/33] set HASURA_GRAPHQL_JWT_SECRET before running the tests --- .circleci/test-server.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh index 29e3c35c047db..5ff7e284b86d1 100755 --- a/.circleci/test-server.sh +++ b/.circleci/test-server.sh @@ -320,13 +320,13 @@ unset HASURA_GRAPHQL_JWT_SECRET ########## echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_namespace_path) #####################################>\n" -TEST_TYPE="jwt" +TEST_TYPE="jwt-with-claims-namespace-path" + +export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasuraClaims"}')" run_hge_with_args serve wait_for_port 8080 -export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasuraClaims"}')" - pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" --hge-jwt-claims-ns-path="$.hasuraClaims" test_jwt.py kill_hge_servers From fe19fd7b06e6997ae90ef1afc182e7c69399b409 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 13:36:23 +0530 Subject: [PATCH 09/33] make a new data type JWTConfigClaims - this type will include `claims_ns` key or `claims_namespace_path` JSON Path --- server/src-lib/Hasura/Server/Auth.hs | 2 +- server/src-lib/Hasura/Server/Auth/JWT.hs | 101 +++++++++++++---------- server/src-lib/Hasura/Server/Config.hs | 23 ++++-- 3 files changed, 77 insertions(+), 49 deletions(-) diff --git a/server/src-lib/Hasura/Server/Auth.hs b/server/src-lib/Hasura/Server/Auth.hs index 2073dbf8cb5ea..3d03864d17b1a 100644 --- a/server/src-lib/Hasura/Server/Auth.hs +++ b/server/src-lib/Hasura/Server/Auth.hs @@ -114,7 +114,7 @@ mkJwtCtx JWTConfig{..} httpManager logger = do Left jwk -> liftIO $ newIORef (JWKSet [jwk]) Right url -> getJwkFromUrl url let claimsFmt = fromMaybe JCFJson jcClaimsFormat - return $ JWTCtx jwkRef jcClaimNs jcClaimNsPath jcAudience claimsFmt jcIssuer + return $ JWTCtx jwkRef jcClaimNs jcAudience claimsFmt jcIssuer where -- if we can't find any expiry time for the JWK (either in @Expires@ header or @Cache-Control@ -- header), do not start a background thread for refreshing the JWK diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index 5ec1322f9d95f..9b6bcde7281be 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -6,6 +6,7 @@ module Hasura.Server.Auth.JWT , Jose.JWKSet (..) , JWTClaimsFormat (..) , JwkFetchError (..) + , JWTConfigClaims (..) , updateJwkRef , jwkRefreshCtrl , defaultClaimNs @@ -61,11 +62,15 @@ data JWTClaimsFormat $(J.deriveJSON J.defaultOptions { J.sumEncoding = J.ObjectWithSingleField , J.constructorTagModifier = J.snakeCase . drop 3 } ''JWTClaimsFormat) +data JWTConfigClaims + = ClaimNsPath JSONPath + | ClaimNs T.Text + deriving (Show, Eq) + data JWTConfig = JWTConfig { jcKeyOrUrl :: !(Either Jose.JWK URI) - , jcClaimNs :: !(Maybe T.Text) - , jcClaimNsPath :: !(Maybe JSONPath) + , jcClaimNs :: !JWTConfigClaims , jcAudience :: !(Maybe Jose.Audience) , jcClaimsFormat :: !(Maybe JWTClaimsFormat) , jcIssuer :: !(Maybe Jose.StringOrURI) @@ -74,16 +79,15 @@ data JWTConfig data JWTCtx = JWTCtx { jcxKey :: !(IORef Jose.JWKSet) - , jcxClaimNs :: !(Maybe T.Text) - , jcxClaimNsPath :: !(Maybe JSONPath) + , jcxClaimNs :: !JWTConfigClaims , jcxAudience :: !(Maybe Jose.Audience) , jcxClaimsFormat :: !JWTClaimsFormat , jcxIssuer :: !(Maybe Jose.StringOrURI) } deriving (Eq) instance Show JWTCtx where - show (JWTCtx _ nsM nsPath audM cf iss) = - show ["", show nsM,show nsPath,show audM, show cf, show iss] + show (JWTCtx _ nsM audM cf iss) = + show ["", show nsM,show audM, show cf, show iss] data HasuraClaims = HasuraClaims @@ -235,16 +239,14 @@ processAuthZHeader jwtCtx headers authzHeader = do -- verify the JWT claims <- liftJWTError invalidJWTError $ verifyJwt jwtCtx $ RawJWT jwt - let claimsNs = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx - claimsFmt = jcxClaimsFormat jwtCtx + let claimsFmt = jcxClaimsFormat jwtCtx expTimeM = fmap (\(Jose.NumericDate t) -> t) $ claims ^. Jose.claimExp - -- see if the hasura claims key exist in the claims map + -- see if the hasura claims key exists in the claims map let mHasuraClaims = - case jcxClaimNsPath jwtCtx of - -- if `claim_namespace_path` is null, then look for hasura claims in the key `claim_namespace` - Nothing -> Map.lookup claimsNs $ claims ^. Jose.unregisteredClaims - Just path -> parseIValueJsonValue $ executeJSONPath path (J.toJSON $ claims ^. Jose.unregisteredClaims) + case jcxClaimNs jwtCtx of + ClaimNs k -> Map.lookup k $ claims ^. Jose.unregisteredClaims + ClaimNsPath path -> parseIValueJsonValue $ executeJSONPath path (J.toJSON $ claims ^. Jose.unregisteredClaims) hasuraClaimsV <- maybe claimsNotFound return mHasuraClaims @@ -286,9 +288,16 @@ processAuthZHeader jwtCtx headers authzHeader = do (JCFJson, _) -> claimsErr "expecting a json object when claims_format is json" - strngfyErr v = "expecting stringified json at: '" - <> fromMaybe defaultClaimNs (jcxClaimNs jwtCtx) - <> "', but found: " <> v + strngfyErr v = + "expecting stringified json at: '" + <> claimsLocation + <> "', but found: " <> v + where + claimsLocation :: Text + claimsLocation = + case jcxClaimNs jwtCtx of + ClaimNsPath path -> T.pack $ "claims_namespace_path " <> JSONPath.formatPath path + ClaimNs ns -> "claims_namespace " <> ns claimsErr = throw400 JWTInvalidClaims @@ -317,8 +326,10 @@ processAuthZHeader jwtCtx headers authzHeader = do currRoleNotAllowed = throw400 AccessDenied "Your current role is not in allowed roles" claimsNotFound = do - let claimsNs = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx - throw400 JWTInvalidClaims $ "claims key: '" <> claimsNs <> "' not found" + let claimsNsError = case jcxClaimNs jwtCtx of + ClaimNsPath path -> T.pack $ "claims not found at claims_namespace_path: '" <> (JSONPath.formatPath path) <> "'" + ClaimNs ns -> "claims key: '" <> ns <> "' not found" + throw400 JWTInvalidClaims $ claimsNsError -- parse x-hasura-allowed-roles, x-hasura-default-role from JWT claims @@ -380,16 +391,20 @@ verifyJwt ctx (RawJWT rawJWT) = do instance J.ToJSON JWTConfig where - toJSON (JWTConfig keyOrUrl claimNs claimNsPath aud claimsFmt iss) = - J.object (jwkFields ++ sharedFields) + toJSON (JWTConfig keyOrUrl claimNs aud claimsFmt iss) = + J.object (jwkFields ++ sharedFields ++ claimsNsFields) where jwkFields = case keyOrUrl of Left _ -> [ "type" J..= J.String "" , "key" J..= J.String "" ] Right url -> [ "jwk_url" J..= url ] - sharedFields = [ "claims_namespace" J..= claimNs - , "claims_namespace_path" J..= maybe Nothing (Just . JSONPath.formatPath) claimNsPath - , "claims_format" J..= claimsFmt + + claimsNsFields = case claimNs of + ClaimNsPath nsPath -> + ["claims_namespace_path" J..= (JSONPath.formatPath nsPath)] + ClaimNs ns -> ["claims_ns" J..= J.String ns] + + sharedFields = [ "claims_format" J..= claimsFmt , "audience" J..= aud , "issuer" J..= iss ] @@ -408,33 +423,33 @@ instance J.FromJSON JWTConfig where jwkUrl <- o J..:? "jwk_url" isStrngfd <- o J..:? "claims_format" - - case claimNsPathStr of - Just nsPathStr -> - let eClaimNsPath = JSONPath.parseJSONPath nsPathStr - in case eClaimNsPath of - Left err -> fail ("invalid JSON path claims_namespace_path " ++ (show nsPathStr) - ++ " error " ++ (show err)) - Right nsPath -> - case (mRawKey, jwkUrl) of - (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" - (Just _, Just _) -> fail "key, jwk_url both cannot be present" - (Just rawKey, Nothing) -> do - keyType <- o J..: "type" - key <- parseKey keyType rawKey - return $ JWTConfig (Left key) claimNs (Just nsPath) aud isStrngfd iss - (Nothing, Just url) -> - return $ JWTConfig (Right url) claimNs (Just nsPath) aud isStrngfd iss - Nothing -> + let hasuraClaimsNs :: Either String JWTConfigClaims + hasuraClaimsNs = + case claimNsPathStr of + Just nsPathStr -> + let eClaimNsPath = JSONPath.parseJSONPath nsPathStr + in + case eClaimNsPath of + Left err -> Left err + Right nsPath -> Right $ ClaimNsPath nsPath + Nothing -> + case claimNs of + Just ns -> Right $ ClaimNs ns + Nothing -> Right $ ClaimNs defaultClaimNs + + case hasuraClaimsNs of + Left err -> fail ("invalid JSON path claims_namespace_path error:" ++ + (show err)) + Right claimsNs -> case (mRawKey, jwkUrl) of (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" (Just _, Just _) -> fail "key, jwk_url both cannot be present" (Just rawKey, Nothing) -> do keyType <- o J..: "type" key <- parseKey keyType rawKey - return $ JWTConfig (Left key) claimNs Nothing aud isStrngfd iss + return $ JWTConfig (Left key) claimsNs aud isStrngfd iss (Nothing, Just url) -> - return $ JWTConfig (Right url) claimNs Nothing aud isStrngfd iss + return $ JWTConfig (Right url) claimsNs aud isStrngfd iss where parseKey keyType rawKey = diff --git a/server/src-lib/Hasura/Server/Config.hs b/server/src-lib/Hasura/Server/Config.hs index 04e772d066397..c4f7034cd1c76 100644 --- a/server/src-lib/Hasura/Server/Config.hs +++ b/server/src-lib/Hasura/Server/Config.hs @@ -12,14 +12,26 @@ import Hasura.Server.Auth.JWT import Hasura.Server.Version (HasVersion, Version, currentVersion) import qualified Hasura.GraphQL.Execute.LiveQuery.Options as LQ +import qualified Data.Aeson as J +import qualified Data.Parser.JSONPath as JSONPath data JWTInfo = JWTInfo - { jwtiClaimsNamespace :: !Text - , jwtiClaimsFormat :: !JWTClaimsFormat + { jwtiClaimsNamespace :: !Text + , jwtiClaimsNamespacePath :: !(Maybe JSONPath.JSONPath) + , jwtiClaimsFormat :: !JWTClaimsFormat } deriving (Show, Eq) -$(deriveToJSON (aesonDrop 4 snakeCase) ''JWTInfo) +instance J.ToJSON JWTInfo where + toJSON (JWTInfo ns (Just nsPath) fmt) = + J.object [ "claims_namespace" J..= J.String ns + , "claims_namespace_path" J..= JSONPath.formatPath nsPath + , "claims_formatPath" J..= fmt + ] + toJSON (JWTInfo ns Nothing fmt) = + J.object [ "claims_namespace" J..= J.String ns + , "claims_formatPath" J..= fmt + ] data ServerConfig = ServerConfig @@ -62,8 +74,9 @@ isJWTSet = \case getJWTInfo :: AuthMode -> Maybe JWTInfo getJWTInfo (AMAdminSecretAndJWT _ jwtCtx _) = - Just $ JWTInfo ns format + case jcxClaimNs jwtCtx of + ClaimNsPath nsPath -> Just $ JWTInfo "" (Just nsPath) format -- Keeping it empty for backward compatibility + ClaimNs ns -> Just $ JWTInfo ns Nothing format where - ns = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx format = jcxClaimsFormat jwtCtx getJWTInfo _ = Nothing From 1d7b5a1b6cf82c29165825fa91e9bdabf7809069 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 14:28:52 +0530 Subject: [PATCH 10/33] modify the encodeJSONPath function to be more robust --- server/src-lib/Data/Parser/JSONPath.hs | 31 ------------------------ server/src-lib/Hasura/RQL/Types/Error.hs | 24 +++++++++++++++--- server/src-lib/Hasura/Server/Auth/JWT.hs | 8 +++--- server/src-lib/Hasura/Server/Config.hs | 3 ++- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/server/src-lib/Data/Parser/JSONPath.hs b/server/src-lib/Data/Parser/JSONPath.hs index efc1c05d44908..dd84a4de8503c 100644 --- a/server/src-lib/Data/Parser/JSONPath.hs +++ b/server/src-lib/Data/Parser/JSONPath.hs @@ -2,7 +2,6 @@ module Data.Parser.JSONPath ( parseJSONPath , JSONPathElement(..) , JSONPath - , formatPath ) where import Control.Applicative ((<|>)) @@ -64,33 +63,3 @@ parseJSONPath = parseResult . parse parseElements invalidMessage s = "invalid property name: " ++ T.unpack s ++ ". Accept letters, digits, underscore (_) or hyphen (-) only" ++ ". Use single quotes enclosed in bracket if there are any special characters" - --- available in Data.Aeson.Types but not importing -formatPath :: JSONPath -> String -formatPath path = "$" ++ formatRelativePath path - -formatRelativePath :: JSONPath -> String -formatRelativePath path = format "" path - where - format :: String -> JSONPath -> String - format pfx [] = pfx - format pfx (Index idx:parts) = format (pfx ++ "[" ++ show idx ++ "]") parts - format pfx (Key key:parts) = format (pfx ++ formatKey key) parts - - formatKey :: T.Text -> String - formatKey key - | isIdentifierKey strKey = "." ++ strKey - | otherwise = "['" ++ escapeKey strKey ++ "']" - where strKey = T.unpack key - - isIdentifierKey :: String -> Bool - isIdentifierKey [] = False - isIdentifierKey (x:xs) = isAlpha x && all isAlphaNum xs - - escapeKey :: String -> String - escapeKey = concatMap escapeChar - - escapeChar :: Char -> String - escapeChar '\'' = "\\'" - escapeChar '\\' = "\\\\" - escapeChar c = [c] diff --git a/server/src-lib/Hasura/RQL/Types/Error.hs b/server/src-lib/Hasura/RQL/Types/Error.hs index 863352d89450c..12d8cbdb68a57 100644 --- a/server/src-lib/Hasura/RQL/Types/Error.hs +++ b/server/src-lib/Hasura/RQL/Types/Error.hs @@ -5,6 +5,7 @@ module Hasura.RQL.Types.Error , QErr(..) , encodeQErr , encodeGQLErr + , encodeJSONPath , noInternalQErrEnc , err400 , err404 @@ -51,6 +52,7 @@ import Data.Aeson.Types import qualified Database.PG.Query as Q import Hasura.Prelude import Text.Show (Show (..)) +import Data.Char import qualified Data.Text as T import qualified Network.HTTP.Types as N @@ -192,13 +194,29 @@ encodeQErr _ = noInternalQErrEnc encodeJSONPath :: JSONPath -> String encodeJSONPath = format "$" where + format :: String -> JSONPath -> String format pfx [] = pfx format pfx (Index idx:parts) = format (pfx ++ "[" ++ show idx ++ "]") parts - format pfx (Key key:parts) = format (pfx ++ "." ++ formatKey key) parts + format pfx (Key key:parts) = format (pfx ++ formatKey key) parts + formatKey :: T.Text -> String formatKey key - | T.any (=='.') key = "['" ++ T.unpack key ++ "']" - | otherwise = T.unpack key + | isIdentifierKey strKey = "." ++ strKey + | otherwise = "['" ++ escapeKey strKey ++ "']" + where strKey = T.unpack key + + isIdentifierKey :: String -> Bool + isIdentifierKey [] = False + isIdentifierKey (x:xs) = isAlpha x && all isAlphaNum xs + + escapeKey :: String -> String + escapeKey = concatMap escapeChar + + escapeChar :: Char -> String + escapeChar '\'' = "\\'" + escapeChar '\\' = "\\\\" + escapeChar c = [c] + instance Q.FromPGConnErr QErr where fromPGConnErr c = diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index 9b6bcde7281be..73b5556de0d3e 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -34,6 +34,7 @@ import Hasura.Server.Auth.JWT.Internal (parseHmacKey, parseRsaKey) import Hasura.Server.Auth.JWT.Logging import Hasura.Server.Utils (getRequestHeader, userRoleHeader, executeJSONPath) import Hasura.Server.Version (HasVersion) +import Hasura.RQL.Types.Error (encodeJSONPath) import qualified Control.Concurrent.Extended as C import qualified Crypto.JWT as Jose @@ -52,6 +53,7 @@ import qualified Network.HTTP.Types as HTTP import qualified Network.Wreq as Wreq import qualified Data.Parser.JSONPath as JSONPath + newtype RawJWT = RawJWT BL.ByteString data JWTClaimsFormat @@ -296,7 +298,7 @@ processAuthZHeader jwtCtx headers authzHeader = do claimsLocation :: Text claimsLocation = case jcxClaimNs jwtCtx of - ClaimNsPath path -> T.pack $ "claims_namespace_path " <> JSONPath.formatPath path + ClaimNsPath path -> T.pack $ "claims_namespace_path " <> encodeJSONPath path ClaimNs ns -> "claims_namespace " <> ns claimsErr = throw400 JWTInvalidClaims @@ -327,7 +329,7 @@ processAuthZHeader jwtCtx headers authzHeader = do throw400 AccessDenied "Your current role is not in allowed roles" claimsNotFound = do let claimsNsError = case jcxClaimNs jwtCtx of - ClaimNsPath path -> T.pack $ "claims not found at claims_namespace_path: '" <> (JSONPath.formatPath path) <> "'" + ClaimNsPath path -> T.pack $ "claims not found at claims_namespace_path: '" <> (encodeJSONPath path) <> "'" ClaimNs ns -> "claims key: '" <> ns <> "' not found" throw400 JWTInvalidClaims $ claimsNsError @@ -401,7 +403,7 @@ instance J.ToJSON JWTConfig where claimsNsFields = case claimNs of ClaimNsPath nsPath -> - ["claims_namespace_path" J..= (JSONPath.formatPath nsPath)] + ["claims_namespace_path" J..= (encodeJSONPath nsPath)] ClaimNs ns -> ["claims_ns" J..= J.String ns] sharedFields = [ "claims_format" J..= claimsFmt diff --git a/server/src-lib/Hasura/Server/Config.hs b/server/src-lib/Hasura/Server/Config.hs index c4f7034cd1c76..65761d000c813 100644 --- a/server/src-lib/Hasura/Server/Config.hs +++ b/server/src-lib/Hasura/Server/Config.hs @@ -10,6 +10,7 @@ import Hasura.Prelude import Hasura.Server.Auth import Hasura.Server.Auth.JWT import Hasura.Server.Version (HasVersion, Version, currentVersion) +import Hasura.RQL.Types.Error (encodeJSONPath) import qualified Hasura.GraphQL.Execute.LiveQuery.Options as LQ import qualified Data.Aeson as J @@ -25,7 +26,7 @@ data JWTInfo instance J.ToJSON JWTInfo where toJSON (JWTInfo ns (Just nsPath) fmt) = J.object [ "claims_namespace" J..= J.String ns - , "claims_namespace_path" J..= JSONPath.formatPath nsPath + , "claims_namespace_path" J..= encodeJSONPath nsPath , "claims_formatPath" J..= fmt ] toJSON (JWTInfo ns Nothing fmt) = From 4fabbcf836a5db9fb159a0bd5c2c8daa732024d2 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 15:55:41 +0530 Subject: [PATCH 11/33] remove unused import from JSONPath.hs --- server/src-lib/Data/Parser/JSONPath.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src-lib/Data/Parser/JSONPath.hs b/server/src-lib/Data/Parser/JSONPath.hs index dd84a4de8503c..694815055ee85 100644 --- a/server/src-lib/Data/Parser/JSONPath.hs +++ b/server/src-lib/Data/Parser/JSONPath.hs @@ -8,7 +8,6 @@ import Control.Applicative ((<|>)) import Data.Aeson.Internal (JSONPath, JSONPathElement (..)) import Data.Attoparsec.Text import Data.Bool (bool) -import Data.Char import Prelude hiding (takeWhile) import Text.Read (readMaybe) import qualified Data.Text as T From 8bca4567a8d50510fea706a47f56ca5425e459ad Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 16:24:33 +0530 Subject: [PATCH 12/33] revert back encodeJSONPath to what it was earlier --- server/src-lib/Hasura/RQL/Types/Error.hs | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/server/src-lib/Hasura/RQL/Types/Error.hs b/server/src-lib/Hasura/RQL/Types/Error.hs index 12d8cbdb68a57..83c9fe37b05dd 100644 --- a/server/src-lib/Hasura/RQL/Types/Error.hs +++ b/server/src-lib/Hasura/RQL/Types/Error.hs @@ -52,7 +52,6 @@ import Data.Aeson.Types import qualified Database.PG.Query as Q import Hasura.Prelude import Text.Show (Show (..)) -import Data.Char import qualified Data.Text as T import qualified Network.HTTP.Types as N @@ -194,29 +193,13 @@ encodeQErr _ = noInternalQErrEnc encodeJSONPath :: JSONPath -> String encodeJSONPath = format "$" where - format :: String -> JSONPath -> String format pfx [] = pfx format pfx (Index idx:parts) = format (pfx ++ "[" ++ show idx ++ "]") parts - format pfx (Key key:parts) = format (pfx ++ formatKey key) parts + format pfx (Key key:parts) = format (pfx ++ "." ++ formatKey key) parts - formatKey :: T.Text -> String formatKey key - | isIdentifierKey strKey = "." ++ strKey - | otherwise = "['" ++ escapeKey strKey ++ "']" - where strKey = T.unpack key - - isIdentifierKey :: String -> Bool - isIdentifierKey [] = False - isIdentifierKey (x:xs) = isAlpha x && all isAlphaNum xs - - escapeKey :: String -> String - escapeKey = concatMap escapeChar - - escapeChar :: Char -> String - escapeChar '\'' = "\\'" - escapeChar '\\' = "\\\\" - escapeChar c = [c] - + | T.any (=='.') key = "['" ++ T.unpack key ++ "']" + | otherwise = T.unpack key instance Q.FromPGConnErr QErr where fromPGConnErr c = From 90ae3f266310b9ff2553ffc143a43f2bb66c97fe Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 17:34:46 +0530 Subject: [PATCH 13/33] modify the test_api_config test to include the claims_namespace_path --- server/src-lib/Hasura/Server/Config.hs | 4 ++-- server/tests-py/context.py | 3 ++- server/tests-py/test_config_api.py | 21 +++++++++++++-------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/server/src-lib/Hasura/Server/Config.hs b/server/src-lib/Hasura/Server/Config.hs index 65761d000c813..984cd2ffe2354 100644 --- a/server/src-lib/Hasura/Server/Config.hs +++ b/server/src-lib/Hasura/Server/Config.hs @@ -27,11 +27,11 @@ instance J.ToJSON JWTInfo where toJSON (JWTInfo ns (Just nsPath) fmt) = J.object [ "claims_namespace" J..= J.String ns , "claims_namespace_path" J..= encodeJSONPath nsPath - , "claims_formatPath" J..= fmt + , "claims_format" J..= fmt ] toJSON (JWTInfo ns Nothing fmt) = J.object [ "claims_namespace" J..= J.String ns - , "claims_formatPath" J..= fmt + , "claims_format" J..= fmt ] data ServerConfig diff --git a/server/tests-py/context.py b/server/tests-py/context.py index c46be3918f568..f9b9d0024c688 100644 --- a/server/tests-py/context.py +++ b/server/tests-py/context.py @@ -406,11 +406,12 @@ class HGECtx: def __init__(self, hge_url, pg_url, config): self.http = requests.Session() - self. hge_key = config.getoption('--hge-key') + self.hge_key = config.getoption('--hge-key') self.hge_url = hge_url self.pg_url = pg_url self.hge_webhook = config.getoption('--hge-webhook') hge_jwt_key_file = config.getoption('--hge-jwt-key-file') + self.hge_jwt_claims_ns_path = config.getoption('--hge-jwt-claims-ns-path') if hge_jwt_key_file is None: self.hge_jwt_key = None else: diff --git a/server/tests-py/test_config_api.py b/server/tests-py/test_config_api.py index 8d2c17237387c..3f017bff9d844 100644 --- a/server/tests-py/test_config_api.py +++ b/server/tests-py/test_config_api.py @@ -1,12 +1,13 @@ import ruamel.yaml as yaml import re +import json class TestConfigAPI(): def test_config_api(self, hge_ctx): admin_secret = hge_ctx.hge_key auth_hook = hge_ctx.hge_webhook - jwt_conf = hge_ctx.hge_jwt_conf + jwt_conf = json.loads(hge_ctx.hge_jwt_conf) headers = {} if admin_secret is not None: @@ -25,14 +26,18 @@ def test_config_api(self, hge_ctx): assert body['is_jwt_set'] == (jwt_conf is not None) if jwt_conf is not None: - claims_namespace = "https://hasura.io/jwt/claims" - if 'claims_namespace' in jwt_conf: - claims_namespace = jwt_conf['claims_namespace'] claims_format = "json" - if 'claims_format' in jwt_conf: - claims_format = jwt_conf['claims_format'] - assert body['jwt']['claims_namespace'] == claims_namespace - assert body['jwt']['claims_format'] == claims_format + if 'claims_namespace_path' in jwt_conf: + assert body['jwt']['claims_namespace_path'] == jwt_conf['claims_namespace_path'] + assert body['jwt']['claims_format'] == claims_format + else: + claims_namespace = "https://hasura.io/jwt/claims" + if 'claims_namespace' in jwt_conf: + claims_namespace = jwt_conf['claims_namespace'] + if 'claims_format' in jwt_conf: + claims_format = jwt_conf['claims_format'] + assert body['jwt']['claims_namespace'] == claims_namespace + assert body['jwt']['claims_format'] == claims_format else: assert body['jwt'] == None From 63a5df685008fd7b9ffc3ff8713009b3c0eb4e83 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 18:02:51 +0530 Subject: [PATCH 14/33] handle the case when hge_jwt_conf is None --- server/tests-py/test_config_api.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/server/tests-py/test_config_api.py b/server/tests-py/test_config_api.py index 3f017bff9d844..1a0e0406c89e8 100644 --- a/server/tests-py/test_config_api.py +++ b/server/tests-py/test_config_api.py @@ -7,7 +7,9 @@ class TestConfigAPI(): def test_config_api(self, hge_ctx): admin_secret = hge_ctx.hge_key auth_hook = hge_ctx.hge_webhook - jwt_conf = json.loads(hge_ctx.hge_jwt_conf) + jwt_conf = hge_ctx.hge_jwt_conf + if jwt_conf is not None: + jwt_conf_dict = json.loads(hge_ctx.hge_jwt_conf) headers = {} if admin_secret is not None: @@ -27,15 +29,15 @@ def test_config_api(self, hge_ctx): if jwt_conf is not None: claims_format = "json" - if 'claims_namespace_path' in jwt_conf: - assert body['jwt']['claims_namespace_path'] == jwt_conf['claims_namespace_path'] + if 'claims_namespace_path' in jwt_conf_dict: + assert body['jwt']['claims_namespace_path'] == jwt_conf_dict['claims_namespace_path'] assert body['jwt']['claims_format'] == claims_format else: claims_namespace = "https://hasura.io/jwt/claims" - if 'claims_namespace' in jwt_conf: - claims_namespace = jwt_conf['claims_namespace'] - if 'claims_format' in jwt_conf: - claims_format = jwt_conf['claims_format'] + if 'claims_namespace' in jwt_conf_dict: + claims_namespace = jwt_conf_dict['claims_namespace'] + if 'claims_format' in jwt_conf_dict: + claims_format = jwt_conf_dict['claims_format'] assert body['jwt']['claims_namespace'] == claims_namespace assert body['jwt']['claims_format'] == claims_format else: From deffb86719ddf752614846ca9788db51692aab18 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 21:14:07 +0530 Subject: [PATCH 15/33] Update docs/graphql/manual/auth/authentication/jwt.rst Co-Authored-By: Marion Schleifer --- docs/graphql/manual/auth/authentication/jwt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index f9298adb1bf74..3b59511298453 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -195,7 +195,7 @@ set to ``$.hasura.claims``: } -This is an optional field, if the value is not provided, then the ``claims_namespace`` value +This is an optional field. If the value is not provided, then the ``claims_namespace`` value is used to get the name of the claims field among the top-level keys. Rotating JWKs From d8989b167c53e11ce318ee9b4875c7ae83f3ce4a Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 21:14:29 +0530 Subject: [PATCH 16/33] Update docs/graphql/manual/auth/authentication/jwt.rst Co-Authored-By: Marion Schleifer --- docs/graphql/manual/auth/authentication/jwt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index 3b59511298453..24850439f41c4 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -173,7 +173,7 @@ encoded public key) as a string - in the ``key`` field along with the ``type``. ^^^^^^^^^^^^^^^^^^^^^^^^^ An optional JSON path value to the hasura claims in the JWT token (e.g. ``$.hasura.claims``) -For example, the JWT token should in this format if the ``claims_namespace_path`` is +The JWT token should be in this format if the ``claims_namespace_path`` is set to ``$.hasura.claims``: .. code-block:: json From 4e4e367078b023f3a68b2827d46a515db3c1a345 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 21:14:48 +0530 Subject: [PATCH 17/33] Update docs/graphql/manual/auth/authentication/jwt.rst Co-Authored-By: Marion Schleifer --- docs/graphql/manual/auth/authentication/jwt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index 24850439f41c4..fed16110d7bd4 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -130,7 +130,7 @@ JSON object: "jwk_url": "", "claims_namespace": "", "claims_format": "json|stringified_json", - "claims_namespace_path":"", + "claims_namespace_path":"", "audience": , "issuer": "" } From bd3f07d4d5ed4e741fdd0e2630d0e8e81eac5583 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 21:15:00 +0530 Subject: [PATCH 18/33] Update docs/graphql/manual/auth/authentication/jwt.rst Co-Authored-By: Marion Schleifer --- docs/graphql/manual/auth/authentication/jwt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index fed16110d7bd4..bccb29291135f 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -171,7 +171,7 @@ encoded public key) as a string - in the ``key`` field along with the ``type``. ``claims_namespace_path`` ^^^^^^^^^^^^^^^^^^^^^^^^^ -An optional JSON path value to the hasura claims in the JWT token (e.g. ``$.hasura.claims``) +An optional JSON path value to the Hasura claims in the JWT token (e.g. ``$.hasura.claims``). The JWT token should be in this format if the ``claims_namespace_path`` is set to ``$.hasura.claims``: From 9a0dca5f97eb474037b2780a8c24a0c18ecff587 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 21:26:57 +0530 Subject: [PATCH 19/33] omit claims_namespace from /v1alpha1/config if claims_namespace_path is set --- server/src-lib/Hasura/Server/Config.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src-lib/Hasura/Server/Config.hs b/server/src-lib/Hasura/Server/Config.hs index 984cd2ffe2354..9192c62efb409 100644 --- a/server/src-lib/Hasura/Server/Config.hs +++ b/server/src-lib/Hasura/Server/Config.hs @@ -24,9 +24,8 @@ data JWTInfo } deriving (Show, Eq) instance J.ToJSON JWTInfo where - toJSON (JWTInfo ns (Just nsPath) fmt) = - J.object [ "claims_namespace" J..= J.String ns - , "claims_namespace_path" J..= encodeJSONPath nsPath + toJSON (JWTInfo _ (Just nsPath) fmt) = + J.object [ "claims_namespace_path" J..= encodeJSONPath nsPath , "claims_format" J..= fmt ] toJSON (JWTInfo ns Nothing fmt) = From 649666744eacb2e3eb6cf4ac02e3c96f781cc69d Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Mon, 13 Apr 2020 22:01:57 +0530 Subject: [PATCH 20/33] modify the jwt tests and refactor executeJSONPath --- server/src-lib/Hasura/Server/Utils.hs | 1 - server/tests-py/test_jwt.py | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index 2c01e5a41489d..c18c3deb9d2ac 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -235,7 +235,6 @@ executeJSONPath jsonPath = iparse (valueParser jsonPath) where valueParser path value = case path of [] -> pure value - [pathElement] -> parseWithPathElement pathElement value (pathElement:remaining) -> parseWithPathElement pathElement value >>= (( pathElement) . valueParser remaining) where diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index b1bf5ce9d6528..aee62420535cb 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -21,8 +21,6 @@ if not PytestConf.config.getoption('--hge-jwt-conf'): pytest.skip('--hge-jwt-key-conf is missing, skipping JWT tests', allow_module_level=True) -claims_namespace_path = PytestConf.config.getoption('--hge-jwt-claims-ns-path') - def get_claims_fmt(raw_conf): conf = json.loads(raw_conf) try: @@ -49,6 +47,7 @@ def test_jwt_valid_claims_success(self, hge_ctx, endpoint): 'x-hasura-allowed-roles': ['user', 'editor'], 'x-hasura-default-role': 'user' }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -65,6 +64,7 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): 'x-hasura-allowed-roles': ['contractor', 'editor'], 'x-hasura-default-role': 'contractor' }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -92,6 +92,7 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user' }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -120,6 +121,7 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): 'x-hasura-allowed-roles': 'user', 'x-hasura-default-role': 'user' }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -147,6 +149,7 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -175,6 +178,7 @@ def test_jwt_expired(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -206,6 +210,7 @@ def test_jwt_invalid_signature(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -239,6 +244,7 @@ def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -259,6 +265,7 @@ def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -320,6 +327,7 @@ def test_jwt_expiry(self, hge_ctx, ws_client): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -351,6 +359,7 @@ def test_jwt_valid_audience(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -371,6 +380,7 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -432,6 +442,7 @@ def test_jwt_valid_issuer(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": @@ -453,6 +464,7 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) + claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path if claims_namespace_path is None: self.claims['https://hasura.io/jwt/claims'] = claims elif claims_namespace_path == "$.hasuraClaims": From d6227756cca5911a2ef91dd2200b52b6bbdc3dff Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 14:00:03 +0530 Subject: [PATCH 21/33] modify the JWTInfo to make claims_namespace as an optional field --- server/src-lib/Hasura/Server/Config.hs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/server/src-lib/Hasura/Server/Config.hs b/server/src-lib/Hasura/Server/Config.hs index 9192c62efb409..cd992580360ee 100644 --- a/server/src-lib/Hasura/Server/Config.hs +++ b/server/src-lib/Hasura/Server/Config.hs @@ -11,27 +11,18 @@ import Hasura.Server.Auth import Hasura.Server.Auth.JWT import Hasura.Server.Version (HasVersion, Version, currentVersion) import Hasura.RQL.Types.Error (encodeJSONPath) +import Data.Text (pack) import qualified Hasura.GraphQL.Execute.LiveQuery.Options as LQ -import qualified Data.Aeson as J -import qualified Data.Parser.JSONPath as JSONPath data JWTInfo = JWTInfo - { jwtiClaimsNamespace :: !Text - , jwtiClaimsNamespacePath :: !(Maybe JSONPath.JSONPath) + { jwtiClaimsNamespace :: !(Maybe Text) + , jwtiClaimsNamespacePath :: !(Maybe Text) , jwtiClaimsFormat :: !JWTClaimsFormat } deriving (Show, Eq) -instance J.ToJSON JWTInfo where - toJSON (JWTInfo _ (Just nsPath) fmt) = - J.object [ "claims_namespace_path" J..= encodeJSONPath nsPath - , "claims_format" J..= fmt - ] - toJSON (JWTInfo ns Nothing fmt) = - J.object [ "claims_namespace" J..= J.String ns - , "claims_format" J..= fmt - ] +$(deriveToJSON (aesonDrop 4 snakeCase) ''JWTInfo) data ServerConfig = ServerConfig @@ -56,7 +47,6 @@ runGetConfig am isAllowListEnabled liveQueryOpts = ServerConfig isAllowListEnabled liveQueryOpts - isAdminSecretSet :: AuthMode -> Bool isAdminSecretSet = \case AMNoAuth -> False @@ -75,8 +65,8 @@ isJWTSet = \case getJWTInfo :: AuthMode -> Maybe JWTInfo getJWTInfo (AMAdminSecretAndJWT _ jwtCtx _) = case jcxClaimNs jwtCtx of - ClaimNsPath nsPath -> Just $ JWTInfo "" (Just nsPath) format -- Keeping it empty for backward compatibility - ClaimNs ns -> Just $ JWTInfo ns Nothing format + ClaimNsPath nsPath -> Just $ JWTInfo Nothing (Just . pack $ encodeJSONPath nsPath) format + ClaimNs ns -> Just $ JWTInfo (Just ns) Nothing format where format = jcxClaimsFormat jwtCtx getJWTInfo _ = Nothing From e4ba4323498f3b5d05fcfdf2e22b5f92b0396933 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 18:08:14 +0530 Subject: [PATCH 22/33] refactor the JWT tests - remove the option of setting claims_namespace_path from env and instead parse it from JWT config --- server/tests-py/conftest.py | 4 - server/tests-py/context.py | 3 +- server/tests-py/test_jwt.py | 142 ++++++++++++++---------------------- server/tests-py/validate.py | 5 +- 4 files changed, 57 insertions(+), 97 deletions(-) diff --git a/server/tests-py/conftest.py b/server/tests-py/conftest.py index 7586250da8ab0..7f2e9f189e603 100644 --- a/server/tests-py/conftest.py +++ b/server/tests-py/conftest.py @@ -39,10 +39,6 @@ def pytest_addoption(parser): "--hge-jwt-conf", metavar="HGE_JWT_CONF", help="The JWT conf", required=False ) - parser.addoption( - "--hge-jwt-claims-ns-path", metavar="HGE_JWT_CLAIMS_NS_PATH", help="The JWT conf claims namespace path", required=False - ) - parser.addoption( "--test-cors", action="store_true", required=False, diff --git a/server/tests-py/context.py b/server/tests-py/context.py index f9b9d0024c688..31ff7db52327b 100644 --- a/server/tests-py/context.py +++ b/server/tests-py/context.py @@ -411,13 +411,14 @@ def __init__(self, hge_url, pg_url, config): self.pg_url = pg_url self.hge_webhook = config.getoption('--hge-webhook') hge_jwt_key_file = config.getoption('--hge-jwt-key-file') - self.hge_jwt_claims_ns_path = config.getoption('--hge-jwt-claims-ns-path') if hge_jwt_key_file is None: self.hge_jwt_key = None else: with open(hge_jwt_key_file) as f: self.hge_jwt_key = f.read() self.hge_jwt_conf = config.getoption('--hge-jwt-conf') + if self.hge_jwt_conf is not None: + self.hge_jwt_conf_dict = json.loads(self.hge_jwt_conf) self.webhook_insecure = config.getoption('--test-webhook-insecure') self.metadata_disabled = config.getoption('--test-metadata-disabled') self.may_skip_test_teardown = False diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index aee62420535cb..5e2f87a694c38 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -38,20 +38,29 @@ def mk_claims(conf, claims): else: return claims +def mk_claims_with_namespace_path(claims,hasura_claims,namespace_path): + if namespace_path is None: + claims['https://hasura.io/jwt/claims'] = hasura_claims + elif namespace_path == "$.hasuraClaims": + claims['hasuraClaims'] = hasura_claims + else: + raise Exception( + '''claims_namespace_path should not be anything + other than $.hasuraClaims for testing. The + value of claims_namespace_path was {}'''.format(namespace_path)) + return claims + @pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql']) class TestJWTBasic(): def test_jwt_valid_claims_success(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['user', 'editor'], 'x-hasura-default-role': 'user' }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['url'] = endpoint @@ -59,16 +68,13 @@ def test_jwt_valid_claims_success(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['contractor', 'editor'], 'x-hasura-default-role': 'contractor' }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -88,15 +94,12 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user' }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -116,16 +119,13 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': 'user', 'x-hasura-default-role': 'user' }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -145,15 +145,12 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_no_default_role(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token self.conf['response'] = { @@ -173,16 +170,13 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_expired(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) exp = datetime.utcnow() - timedelta(minutes=1) self.claims['exp'] = round(exp.timestamp()) @@ -205,16 +199,13 @@ def test_jwt_expired(self, hge_ctx, endpoint): check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) def test_jwt_invalid_signature(self, hge_ctx, endpoint): - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) wrong_key = gen_rsa_key() token = jwt.encode(self.claims, wrong_key, algorithm='HS256').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -239,16 +230,13 @@ def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): if 'audience' in jwt_conf: pytest.skip('audience present in conf, skipping testing no audience') - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['aud'] = 'hasura-test-suite' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -260,16 +248,13 @@ def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): if 'issuer' in jwt_conf: pytest.skip('issuer present in conf, skipping testing no issuer') - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['iss'] = 'rubbish-issuer' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -322,16 +307,13 @@ def test_jwt_expiry(self, hge_ctx, ws_client): 'name': 'John Doe', 'iat': math.floor(curr_time.timestamp()) } - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) exp = curr_time + timedelta(seconds=4) self.claims['exp'] = round(exp.timestamp()) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -354,18 +336,14 @@ def test_jwt_valid_audience(self, hge_ctx, endpoint): audience = jwt_conf['audience'] audience = audience if isinstance(audience, str) else audience[0] - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['aud'] = audience - token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path) @@ -375,17 +353,13 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): if 'audience' not in jwt_conf: pytest.skip('audience not present in conf, skipping testing audience') - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims - + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['aud'] = 'rubbish_audience' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -437,17 +411,13 @@ def test_jwt_valid_issuer(self, hge_ctx, endpoint): pytest.skip('issuer not present in conf, skipping testing issuer') issuer = jwt_conf['issuer'] - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims - + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['iss'] = issuer token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -459,17 +429,13 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): if 'issuer' not in jwt_conf: pytest.skip('issuer not present in conf, skipping testing issuer') - claims = mk_claims(hge_ctx.hge_jwt_conf, { + hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, { 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_claims_ns_path - if claims_namespace_path is None: - self.claims['https://hasura.io/jwt/claims'] = claims - elif claims_namespace_path == "$.hasuraClaims": - self.claims['hasuraClaims'] = claims - + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['iss'] = 'rubbish_issuer' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') diff --git a/server/tests-py/validate.py b/server/tests-py/validate.py index 9cb332f26aedd..f82e74cbd96b5 100644 --- a/server/tests-py/validate.py +++ b/server/tests-py/validate.py @@ -151,10 +151,7 @@ def check_query(hge_ctx, conf, transport='http', add_auth=True, claims_namespace "sub": "foo", "name": "bar", } - if claims_namespace_path is None: - claim["https://hasura.io/jwt/claims"] = hClaims - elif claims_namespace_path == "$.hasuraClaims": - claim['hasuraClaims'] = hClaims + claim = mk_claims_with_namespace_path(claim,hClaims,claims_namespace_path) headers['Authorization'] = 'Bearer ' + jwt.encode(claim, hge_ctx.hge_jwt_key, algorithm='RS512').decode( 'UTF-8') From e1b08b3a03ebc14a1733315c938b94e516cc66d4 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 18:22:06 +0530 Subject: [PATCH 23/33] refactor the FromJSON instance of JWTConfig --- server/src-lib/Hasura/Server/Auth/JWT.hs | 50 ++++++++++-------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index 73b5556de0d3e..cbfbec9c4265f 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -404,7 +404,7 @@ instance J.ToJSON JWTConfig where claimsNsFields = case claimNs of ClaimNsPath nsPath -> ["claims_namespace_path" J..= (encodeJSONPath nsPath)] - ClaimNs ns -> ["claims_ns" J..= J.String ns] + ClaimNs ns -> ["claims_namespace" J..= J.String ns] sharedFields = [ "claims_format" J..= claimsFmt , "audience" J..= aud @@ -419,39 +419,28 @@ instance J.FromJSON JWTConfig where parseJSON = J.withObject "JWTConfig" $ \o -> do mRawKey <- o J..:? "key" claimNs <- o J..:? "claims_namespace" - claimNsPathStr <- o J..:? "claims_namespace_path" aud <- o J..:? "audience" iss <- o J..:? "issuer" jwkUrl <- o J..:? "jwk_url" isStrngfd <- o J..:? "claims_format" - let hasuraClaimsNs :: Either String JWTConfigClaims - hasuraClaimsNs = - case claimNsPathStr of - Just nsPathStr -> - let eClaimNsPath = JSONPath.parseJSONPath nsPathStr - in - case eClaimNsPath of - Left err -> Left err - Right nsPath -> Right $ ClaimNsPath nsPath - Nothing -> - case claimNs of - Just ns -> Right $ ClaimNs ns - Nothing -> Right $ ClaimNs defaultClaimNs - - case hasuraClaimsNs of - Left err -> fail ("invalid JSON path claims_namespace_path error:" ++ - (show err)) - Right claimsNs -> - case (mRawKey, jwkUrl) of - (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" - (Just _, Just _) -> fail "key, jwk_url both cannot be present" - (Just rawKey, Nothing) -> do - keyType <- o J..: "type" - key <- parseKey keyType rawKey - return $ JWTConfig (Left key) claimsNs aud isStrngfd iss - (Nothing, Just url) -> - return $ JWTConfig (Right url) claimsNs aud isStrngfd iss + maybeClaimNsPath <- (o J..:? "claims_namespace_path") >>= + mapM (either failJSONPathParsing pure . JSONPath.parseJSONPath) + + + let hasuraClaimsNs = case maybeClaimNsPath of + Just claimsNsPath -> ClaimNsPath claimsNsPath + Nothing -> ClaimNs $ fromMaybe defaultClaimNs claimNs + + case (mRawKey, jwkUrl) of + (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" + (Just _, Just _) -> fail "key, jwk_url both cannot be present" + (Just rawKey, Nothing) -> do + keyType <- o J..: "type" + key <- parseKey keyType rawKey + return $ JWTConfig (Left key) hasuraClaimsNs aud isStrngfd iss + (Nothing, Just url) -> + return $ JWTConfig (Right url) hasuraClaimsNs aud isStrngfd iss where parseKey keyType rawKey = @@ -466,4 +455,7 @@ instance J.FromJSON JWTConfig where _ -> invalidJwk ("Key type: " <> T.unpack keyType <> " is not supported") runEither = either (invalidJwk . T.unpack) return + invalidJwk msg = fail ("Invalid JWK: " <> msg) + + failJSONPathParsing err = fail $ "invalid JSON path claims_namespace_path error: " ++ err From c9b84e86acff679abdf404c01d1e458b67ad23b8 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 20:01:57 +0530 Subject: [PATCH 24/33] use JWTConfigClaims in the JWTInfo data type --- server/src-lib/Hasura/Server/Auth/JWT.hs | 5 ++++- server/src-lib/Hasura/Server/Config.hs | 10 +++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index cbfbec9c4265f..d939329fcc901 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -53,7 +53,6 @@ import qualified Network.HTTP.Types as HTTP import qualified Network.Wreq as Wreq import qualified Data.Parser.JSONPath as JSONPath - newtype RawJWT = RawJWT BL.ByteString data JWTClaimsFormat @@ -69,6 +68,10 @@ data JWTConfigClaims | ClaimNs T.Text deriving (Show, Eq) +instance J.ToJSON JWTConfigClaims where + toJSON (ClaimNsPath nsPath) = J.String . T.pack $ encodeJSONPath nsPath + toJSON (ClaimNs ns) = J.String ns + data JWTConfig = JWTConfig { jcKeyOrUrl :: !(Either Jose.JWK URI) diff --git a/server/src-lib/Hasura/Server/Config.hs b/server/src-lib/Hasura/Server/Config.hs index cd992580360ee..c72fe8b1f8daf 100644 --- a/server/src-lib/Hasura/Server/Config.hs +++ b/server/src-lib/Hasura/Server/Config.hs @@ -10,15 +10,12 @@ import Hasura.Prelude import Hasura.Server.Auth import Hasura.Server.Auth.JWT import Hasura.Server.Version (HasVersion, Version, currentVersion) -import Hasura.RQL.Types.Error (encodeJSONPath) -import Data.Text (pack) import qualified Hasura.GraphQL.Execute.LiveQuery.Options as LQ data JWTInfo = JWTInfo - { jwtiClaimsNamespace :: !(Maybe Text) - , jwtiClaimsNamespacePath :: !(Maybe Text) + { jwtiClaimsNamespace :: !JWTConfigClaims , jwtiClaimsFormat :: !JWTClaimsFormat } deriving (Show, Eq) @@ -64,9 +61,8 @@ isJWTSet = \case getJWTInfo :: AuthMode -> Maybe JWTInfo getJWTInfo (AMAdminSecretAndJWT _ jwtCtx _) = - case jcxClaimNs jwtCtx of - ClaimNsPath nsPath -> Just $ JWTInfo Nothing (Just . pack $ encodeJSONPath nsPath) format - ClaimNs ns -> Just $ JWTInfo (Just ns) Nothing format + Just $ JWTInfo claimsNs format where + claimsNs = jcxClaimNs jwtCtx format = jcxClaimsFormat jwtCtx getJWTInfo _ = Nothing From bf8dda037c40c36c267ff96a49babe3c36113163 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 20:10:47 +0530 Subject: [PATCH 25/33] remove hge-jwt-claims-ns-path from CI claims_namespace_path test --- .circleci/test-server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh index 5ff7e284b86d1..2c31a49c12217 100755 --- a/.circleci/test-server.sh +++ b/.circleci/test-server.sh @@ -327,7 +327,7 @@ export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jw run_hge_with_args serve wait_for_port 8080 -pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" --hge-jwt-claims-ns-path="$.hasuraClaims" test_jwt.py +pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" test_jwt.py kill_hge_servers From 30c1cb5da94027ff05b9022ce440f5165433abe3 Mon Sep 17 00:00:00 2001 From: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:07:09 +0530 Subject: [PATCH 26/33] improve encodeJSONPath, add property tests for parseJSONPath --- server/graphql-engine.cabal | 4 ++- server/src-lib/Data/URL/Template.hs | 4 +-- server/src-lib/Hasura/Prelude.hs | 2 +- server/src-lib/Hasura/RQL/Types/Error.hs | 6 ++-- server/src-test/Data/Parser/JsonPath.hs | 31 +++++++++++++++++++ server/src-test/Main.hs | 2 ++ .../query_illegal_cast_is_not_allowed.yaml | 2 +- .../user_cannot_update_id_col_article.yaml | 2 +- .../user_update_resident_preset_error.yaml | 8 ++--- 9 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 server/src-test/Data/Parser/JsonPath.hs diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 94a735796ca1a..486163c198ad0 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -231,6 +231,7 @@ library , Hasura.Server.PGDump -- Exposed for testing: , Hasura.Server.Telemetry.Counters + , Data.Parser.JSONPath , Hasura.RQL.Types , Hasura.RQL.Types.Run @@ -374,7 +375,6 @@ library , Data.List.Extended , Data.HashMap.Strict.Extended , Data.HashMap.Strict.InsOrd.Extended - , Data.Parser.JSONPath , Data.Sequence.NonEmpty , Data.TByteString , Data.Text.Extended @@ -426,11 +426,13 @@ test-suite graphql-engine-tests , time , transformers-base , unordered-containers + , text hs-source-dirs: src-test main-is: Main.hs other-modules: Data.Parser.CacheControlSpec Data.Parser.URLTemplate + Data.Parser.JsonPath Data.TimeSpec Hasura.IncrementalSpec Hasura.RQL.MetadataSpec diff --git a/server/src-lib/Data/URL/Template.hs b/server/src-lib/Data/URL/Template.hs index e9fba28113bfa..a364cbd0d4b89 100644 --- a/server/src-lib/Data/URL/Template.hs +++ b/server/src-lib/Data/URL/Template.hs @@ -82,12 +82,12 @@ renderURLTemplate template = do -- QuickCheck generators instance Arbitrary Variable where - arbitrary = Variable . T.pack <$> listOf1 (elements $ alphaNumerics <> "-_") + arbitrary = Variable . T.pack <$> listOf1 (elements $ alphaNumerics <> " -_") instance Arbitrary URLTemplate where arbitrary = URLTemplate <$> listOf (oneof [genText, genVariable]) where - genText = (TIText . T.pack) <$> listOf1 (elements $ alphaNumerics <> "://") + genText = (TIText . T.pack) <$> listOf1 (elements $ alphaNumerics <> " ://") genVariable = TIVariable <$> arbitrary genURLTemplate :: Gen URLTemplate diff --git a/server/src-lib/Hasura/Prelude.hs b/server/src-lib/Hasura/Prelude.hs index 87e71b978836d..c4ff0380b48ac 100644 --- a/server/src-lib/Hasura/Prelude.hs +++ b/server/src-lib/Hasura/Prelude.hs @@ -72,7 +72,7 @@ import qualified GHC.Clock as Clock import qualified Test.QuickCheck as QC alphaNumerics :: String -alphaNumerics = ['a'..'z'] ++ ['A'..'Z'] ++ "0123456789 " +alphaNumerics = ['a'..'z'] ++ ['A'..'Z'] ++ "0123456789" instance Arbitrary Text where arbitrary = T.pack <$> QC.listOf (QC.elements alphaNumerics) diff --git a/server/src-lib/Hasura/RQL/Types/Error.hs b/server/src-lib/Hasura/RQL/Types/Error.hs index 83c9fe37b05dd..980f1f09f289c 100644 --- a/server/src-lib/Hasura/RQL/Types/Error.hs +++ b/server/src-lib/Hasura/RQL/Types/Error.hs @@ -198,8 +198,10 @@ encodeJSONPath = format "$" format pfx (Key key:parts) = format (pfx ++ "." ++ formatKey key) parts formatKey key - | T.any (=='.') key = "['" ++ T.unpack key ++ "']" - | otherwise = T.unpack key + | T.any specialChar key = "['" ++ T.unpack key ++ "']" + | otherwise = T.unpack key + where + specialChar = flip notElem (alphaNumerics ++ "_-") instance Q.FromPGConnErr QErr where fromPGConnErr c = diff --git a/server/src-test/Data/Parser/JsonPath.hs b/server/src-test/Data/Parser/JsonPath.hs new file mode 100644 index 0000000000000..082b4fbe490fa --- /dev/null +++ b/server/src-test/Data/Parser/JsonPath.hs @@ -0,0 +1,31 @@ +module Data.Parser.JsonPath (spec) where + +import Hasura.Prelude +import Hasura.RQL.Types (encodeJSONPath) + +import Data.Parser.JSONPath +import Test.Hspec +import Test.QuickCheck + +import qualified Data.Text as T + +spec :: Spec +spec = describe "parseJSONPath" $ + it "JSONPath parser" $ + withMaxSuccess 1000 $ + forAll(resize 20 generateJSONPath) $ \jsonPath -> + let encPath = encodeJSONPath jsonPath + parsedJSONPathE = parseJSONPath $ T.pack encPath + in case parsedJSONPathE of + Left err -> counterexample (err <> ": " <> encPath) False + Right parsedJSONPath -> property $ parsedJSONPath == jsonPath + +generateJSONPath :: Gen JSONPath +generateJSONPath = map (either id id) <$> listOf1 genPathElementEither + where + genPathElementEither = do + indexLeft <- Left <$> genIndex + keyRight <- Right <$> genKey + elements [indexLeft, keyRight] + genIndex = Index <$> choose (0, 100) + genKey = (Key . T.pack) <$> listOf1 (elements $ alphaNumerics ++ ".,!@#$%^&*_-?:;|/\"") diff --git a/server/src-test/Main.hs b/server/src-test/Main.hs index e66b73e6e9f17..2d21911f7ac14 100644 --- a/server/src-test/Main.hs +++ b/server/src-test/Main.hs @@ -26,6 +26,7 @@ import Hasura.Server.Migrate import Hasura.Server.Version import qualified Data.Parser.CacheControlSpec as CacheControlParser +import qualified Data.Parser.JsonPath as JsonPath import qualified Data.Parser.URLTemplate as URLTemplate import qualified Data.TimeSpec as TimeSpec import qualified Hasura.IncrementalSpec as IncrementalSpec @@ -57,6 +58,7 @@ unitSpecs :: Spec unitSpecs = do describe "Data.Parser.CacheControl" CacheControlParser.spec describe "Data.Parser.URLTemplate" URLTemplate.spec + describe "Data.Parser.JsonPath" JsonPath.spec describe "Hasura.Incremental" IncrementalSpec.spec -- describe "Hasura.RQL.Metadata" MetadataSpec.spec -- Commenting until optimizing the test in CI describe "Data.Time" TimeSpec.spec diff --git a/server/tests-py/queries/v1/select/boolexp/postgis/query_illegal_cast_is_not_allowed.yaml b/server/tests-py/queries/v1/select/boolexp/postgis/query_illegal_cast_is_not_allowed.yaml index 8e7d5b8d7ff72..4e285f03fbcc9 100644 --- a/server/tests-py/queries/v1/select/boolexp/postgis/query_illegal_cast_is_not_allowed.yaml +++ b/server/tests-py/queries/v1/select/boolexp/postgis/query_illegal_cast_is_not_allowed.yaml @@ -4,7 +4,7 @@ status: 400 response: code: unexpected-payload error: cannot cast column of type "geography" to type "integer" - path: $.args.where.geog_col.$cast + path: $.args.where.geog_col.['$cast'] query: type: select args: diff --git a/server/tests-py/queries/v1/update/permissions/user_cannot_update_id_col_article.yaml b/server/tests-py/queries/v1/update/permissions/user_cannot_update_id_col_article.yaml index b15903e7cffb4..13a2acb135273 100644 --- a/server/tests-py/queries/v1/update/permissions/user_cannot_update_id_col_article.yaml +++ b/server/tests-py/queries/v1/update/permissions/user_cannot_update_id_col_article.yaml @@ -5,7 +5,7 @@ headers: X-Hasura-User-Id: '1' status: 400 response: - path: $.args.$set + path: $.args.['$set'] error: role "user" does not have permission to update column "id" code: permission-denied query: diff --git a/server/tests-py/queries/v1/update/permissions/user_update_resident_preset_error.yaml b/server/tests-py/queries/v1/update/permissions/user_update_resident_preset_error.yaml index a2b0403950dfb..3de948c831cb6 100644 --- a/server/tests-py/queries/v1/update/permissions/user_update_resident_preset_error.yaml +++ b/server/tests-py/queries/v1/update/permissions/user_update_resident_preset_error.yaml @@ -4,15 +4,15 @@ status: 400 headers: X-Hasura-Role: user1 response: - path: "$.args.$set" - error: column "city" is not updatable for role "user1"; its value is predefined in - permission + path: $.args.['$set'] + error: column "city" is not updatable for role "user1"; its value is predefined + in permission code: not-supported query: type: update args: table: resident - "$set": + $set: city: hobart where: name: clarke From 0dec4f018dcb628ac18c7a0d434b5b1222d20317 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 21:14:54 +0530 Subject: [PATCH 27/33] throw error if both claims_namespace_path and claims_namespace are set --- server/src-lib/Hasura/Server/Auth/JWT.hs | 15 ++++++++------- server/tests-py/test_jwt.py | 14 +------------- server/tests-py/validate.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index d939329fcc901..b4568005aac0d 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -421,19 +421,20 @@ instance J.FromJSON JWTConfig where parseJSON = J.withObject "JWTConfig" $ \o -> do mRawKey <- o J..:? "key" - claimNs <- o J..:? "claims_namespace" + claimsNs <- o J..:? "claims_namespace" + claimsNsPath <- o J..:? "claims_namespace_path" aud <- o J..:? "audience" iss <- o J..:? "issuer" jwkUrl <- o J..:? "jwk_url" isStrngfd <- o J..:? "claims_format" - maybeClaimNsPath <- (o J..:? "claims_namespace_path") >>= - mapM (either failJSONPathParsing pure . JSONPath.parseJSONPath) - - let hasuraClaimsNs = case maybeClaimNsPath of - Just claimsNsPath -> ClaimNsPath claimsNsPath - Nothing -> ClaimNs $ fromMaybe defaultClaimNs claimNs + hasuraClaimsNs <- + case (claimsNsPath,claimsNs) of + (Nothing, Nothing) -> return $ ClaimNs defaultClaimNs + (Just nsPath, Nothing) -> either failJSONPathParsing (return . ClaimNsPath) . JSONPath.parseJSONPath $ nsPath + (Nothing, Just ns) -> return $ ClaimNs ns + (Just _, Just _) -> fail "claims_namespace and claims_namespace_path both cannot be set" case (mRawKey, jwkUrl) of (Nothing, Nothing) -> fail "key and jwk_url both cannot be empty" diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index 5e2f87a694c38..367e30e747230 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -11,7 +11,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization -from validate import check_query +from validate import check_query, mk_claims_with_namespace_path from context import PytestConf @@ -38,18 +38,6 @@ def mk_claims(conf, claims): else: return claims -def mk_claims_with_namespace_path(claims,hasura_claims,namespace_path): - if namespace_path is None: - claims['https://hasura.io/jwt/claims'] = hasura_claims - elif namespace_path == "$.hasuraClaims": - claims['hasuraClaims'] = hasura_claims - else: - raise Exception( - '''claims_namespace_path should not be anything - other than $.hasuraClaims for testing. The - value of claims_namespace_path was {}'''.format(namespace_path)) - return claims - @pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql']) class TestJWTBasic(): diff --git a/server/tests-py/validate.py b/server/tests-py/validate.py index f82e74cbd96b5..080103a60a410 100644 --- a/server/tests-py/validate.py +++ b/server/tests-py/validate.py @@ -123,6 +123,18 @@ def test_forbidden_webhook(hge_ctx, conf): }) +def mk_claims_with_namespace_path(claims,hasura_claims,namespace_path): + if namespace_path is None: + claims['https://hasura.io/jwt/claims'] = hasura_claims + elif namespace_path == "$.hasuraClaims": + claims['hasuraClaims'] = hasura_claims + else: + raise Exception( + '''claims_namespace_path should not be anything + other than $.hasuraClaims for testing. The + value of claims_namespace_path was {}'''.format(namespace_path)) + return claims + # Returns the response received and a bool indicating whether the test passed # or not (this will always be True unless we are `--accepting`) def check_query(hge_ctx, conf, transport='http', add_auth=True, claims_namespace_path=None): From f81062df506f86728fac590b40d46d8667c59702 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 21:43:20 +0530 Subject: [PATCH 28/33] refactor the Data.Parser.JsonPath to Data.Parser.JSONPathSpec --- server/graphql-engine.cabal | 2 +- server/src-test/Data/Parser/{JsonPath.hs => JSONPathSpec.hs} | 2 +- server/src-test/Main.hs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename server/src-test/Data/Parser/{JsonPath.hs => JSONPathSpec.hs} (95%) diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 486163c198ad0..01a79e1ca211c 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -432,7 +432,7 @@ test-suite graphql-engine-tests other-modules: Data.Parser.CacheControlSpec Data.Parser.URLTemplate - Data.Parser.JsonPath + Data.Parser.JSONPathSpec Data.TimeSpec Hasura.IncrementalSpec Hasura.RQL.MetadataSpec diff --git a/server/src-test/Data/Parser/JsonPath.hs b/server/src-test/Data/Parser/JSONPathSpec.hs similarity index 95% rename from server/src-test/Data/Parser/JsonPath.hs rename to server/src-test/Data/Parser/JSONPathSpec.hs index 082b4fbe490fa..4bfa91a012231 100644 --- a/server/src-test/Data/Parser/JsonPath.hs +++ b/server/src-test/Data/Parser/JSONPathSpec.hs @@ -1,4 +1,4 @@ -module Data.Parser.JsonPath (spec) where +module Data.Parser.JSONPathSpec (spec) where import Hasura.Prelude import Hasura.RQL.Types (encodeJSONPath) diff --git a/server/src-test/Main.hs b/server/src-test/Main.hs index 2d21911f7ac14..c1f3cfc9bda8b 100644 --- a/server/src-test/Main.hs +++ b/server/src-test/Main.hs @@ -26,7 +26,7 @@ import Hasura.Server.Migrate import Hasura.Server.Version import qualified Data.Parser.CacheControlSpec as CacheControlParser -import qualified Data.Parser.JsonPath as JsonPath +import qualified Data.Parser.JSONPathSpec as JsonPath import qualified Data.Parser.URLTemplate as URLTemplate import qualified Data.TimeSpec as TimeSpec import qualified Hasura.IncrementalSpec as IncrementalSpec From 0fa2a0207ce2705adb84f52643d90a769bd75113 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 22:04:08 +0530 Subject: [PATCH 29/33] update the JWT docs --- docs/graphql/manual/auth/authentication/jwt.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index bccb29291135f..3096a3431b118 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -92,7 +92,8 @@ etc.) JWT claims, as well as Hasura specific claims inside a custom namespace (or key) i.e. ``https://hasura.io/jwt/claims``. The ``https://hasura.io/jwt/claims`` is the custom namespace where all Hasura -specific claims have to be present. This value can be configured in the JWT +specific claims have to be present. This value can be configured using +``claims_namespace`` or ``claims_namespace_path`` in the JWT config while starting the server. **Note**: ``x-hasura-default-role`` and ``x-hasura-allowed-roles`` are @@ -103,6 +104,14 @@ mandatory, while the rest of them are optional. All ``x-hasura-*`` values should be of type ``String``, they will be converted to the right type automatically. +.. note:: + + The JWT config should either have the ``claims_namespace`` or ``claims_namespace_path`` + value set. If both the keys are missing, then the ``claims_namespace`` will default to + ``https://hasura.io/jwt/claims`` and if both are set then the server will throw a fatal + error and not start. + + The default role can be overridden by the ``x-hasura-role`` header, while making a request. @@ -195,9 +204,6 @@ set to ``$.hasura.claims``: } -This is an optional field. If the value is not provided, then the ``claims_namespace`` value -is used to get the name of the claims field among the top-level keys. - Rotating JWKs +++++++++++++ From 736955ff1d78e2243c2e1a6fb6faeb1b520c33c7 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 15 Apr 2020 22:38:19 +0530 Subject: [PATCH 30/33] get claims_namespace_path from JWT config only if it's set --- server/tests-py/test_jwt.py | 69 +++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py index 367e30e747230..74aa08ea26f78 100644 --- a/server/tests-py/test_jwt.py +++ b/server/tests-py/test_jwt.py @@ -47,7 +47,9 @@ def test_jwt_valid_claims_success(self, hge_ctx, endpoint): 'x-hasura-allowed-roles': ['user', 'editor'], 'x-hasura-default-role': 'user' }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -61,7 +63,10 @@ def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint): 'x-hasura-allowed-roles': ['contractor', 'editor'], 'x-hasura-default-role': 'contractor' }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -86,7 +91,10 @@ def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint): 'x-hasura-user-id': '1', 'x-hasura-default-role': 'user' }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -112,7 +120,10 @@ def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint): 'x-hasura-allowed-roles': 'user', 'x-hasura-default-role': 'user' }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -137,7 +148,10 @@ def test_jwt_no_default_role(self, hge_ctx, endpoint): 'x-hasura-user-id': '1', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') self.conf['headers']['Authorization'] = 'Bearer ' + token @@ -163,7 +177,10 @@ def test_jwt_expired(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) exp = datetime.utcnow() - timedelta(minutes=1) self.claims['exp'] = round(exp.timestamp()) @@ -192,7 +209,10 @@ def test_jwt_invalid_signature(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) wrong_key = gen_rsa_key() token = jwt.encode(self.claims, wrong_key, algorithm='HS256').decode('utf-8') @@ -223,7 +243,10 @@ def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['aud'] = 'hasura-test-suite' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -241,7 +264,10 @@ def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['iss'] = 'rubbish-issuer' token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -300,7 +326,10 @@ def test_jwt_expiry(self, hge_ctx, ws_client): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) exp = curr_time + timedelta(seconds=4) self.claims['exp'] = round(exp.timestamp()) @@ -329,7 +358,10 @@ def test_jwt_valid_audience(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['aud'] = audience token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8') @@ -346,7 +378,10 @@ def test_jwt_invalid_audience(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['aud'] = 'rubbish_audience' @@ -404,7 +439,10 @@ def test_jwt_valid_issuer(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['iss'] = issuer @@ -422,7 +460,10 @@ def test_jwt_invalid_issuer(self, hge_ctx, endpoint): 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], }) - claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + claims_namespace_path = None + if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict: + claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path'] + self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path) self.claims['iss'] = 'rubbish_issuer' From ba444ffec5560fdafdc605723ae0c126dfd941df Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Thu, 16 Apr 2020 10:14:25 +0530 Subject: [PATCH 31/33] modify the JWT docs --- .../manual/auth/authentication/jwt.rst | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index 3096a3431b118..c2cadd3e90d84 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -104,14 +104,6 @@ mandatory, while the rest of them are optional. All ``x-hasura-*`` values should be of type ``String``, they will be converted to the right type automatically. -.. note:: - - The JWT config should either have the ``claims_namespace`` or ``claims_namespace_path`` - value set. If both the keys are missing, then the ``claims_namespace`` will default to - ``https://hasura.io/jwt/claims`` and if both are set then the server will throw a fatal - error and not start. - - The default role can be overridden by the ``x-hasura-role`` header, while making a request. @@ -138,8 +130,8 @@ JSON object: "key": "", "jwk_url": "", "claims_namespace": "", - "claims_format": "json|stringified_json", "claims_namespace_path":"", + "claims_format": "json|stringified_json", "audience": , "issuer": "" } @@ -178,32 +170,6 @@ https://tools.ietf.org/html/rfc7517. This is an optional field. You can also provide the key (certificate, PEM encoded public key) as a string - in the ``key`` field along with the ``type``. -``claims_namespace_path`` -^^^^^^^^^^^^^^^^^^^^^^^^^ -An optional JSON path value to the Hasura claims in the JWT token (e.g. ``$.hasura.claims``). - -The JWT token should be in this format if the ``claims_namespace_path`` is -set to ``$.hasura.claims``: - -.. code-block:: json - - { - "sub": "1234567890", - "name": "John Doe", - "admin": true, - "iat": 1516239022, - "hasura": { - "claims": { - "x-hasura-allowed-roles": ["editor","user", "mod"], - "x-hasura-default-role": "user", - "x-hasura-user-id": "1234567890", - "x-hasura-org-id": "123", - "x-hasura-custom": "custom-value" - } - } - } - - Rotating JWKs +++++++++++++ @@ -256,6 +222,38 @@ inside which the Hasura specific claims will be present, e.g. ``https://mydomain **Default value** is: ``https://hasura.io/jwt/claims``. +``claims_namespace_path`` +^^^^^^^^^^^^^^^^^^^^^^^^^ +An optional JSON path value to the Hasura claims in the JWT token. + +Example values are ``$.hasura.claims`` or ``$`` (i.e. root of the payload) + +The JWT token should be in this format if the ``claims_namespace_path`` is +set to ``$.hasura.claims``: + +.. code-block:: json + + { + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "iat": 1516239022, + "hasura": { + "claims": { + "x-hasura-allowed-roles": ["editor","user", "mod"], + "x-hasura-default-role": "user", + "x-hasura-user-id": "1234567890", + "x-hasura-org-id": "123", + "x-hasura-custom": "custom-value" + } + } + } + +.. note:: + + The JWT config can only have one of claims_namespace or claims_namespace_path values set. + If neither keys are set, then the default value of + claims_namespace i.e. https://hasura.io/jwt/claims will be used. ``claims_format`` ^^^^^^^^^^^^^^^^^ From 966c6a564ee14e143c7753be7e9c750ce36c3a60 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Thu, 16 Apr 2020 10:58:12 +0530 Subject: [PATCH 32/33] modify the JWT docs --- docs/graphql/manual/auth/authentication/jwt.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index c2cadd3e90d84..e442c7c3212ae 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -251,8 +251,8 @@ set to ``$.hasura.claims``: .. note:: - The JWT config can only have one of claims_namespace or claims_namespace_path values set. - If neither keys are set, then the default value of + The JWT config can only have one of ``claims_namespace`` or ``claims_namespace_path`` + values set. If neither keys are set, then the default value of claims_namespace i.e. https://hasura.io/jwt/claims will be used. ``claims_format`` From 7123319ec497d558e0ef13c706124bc18825a4be Mon Sep 17 00:00:00 2001 From: Tirumarai Selvan Date: Thu, 16 Apr 2020 11:36:14 +0530 Subject: [PATCH 33/33] Update docs/graphql/manual/auth/authentication/jwt.rst Co-Authored-By: Marion Schleifer --- docs/graphql/manual/auth/authentication/jwt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst index e442c7c3212ae..a30d4dd0db1b7 100644 --- a/docs/graphql/manual/auth/authentication/jwt.rst +++ b/docs/graphql/manual/auth/authentication/jwt.rst @@ -253,7 +253,7 @@ set to ``$.hasura.claims``: The JWT config can only have one of ``claims_namespace`` or ``claims_namespace_path`` values set. If neither keys are set, then the default value of - claims_namespace i.e. https://hasura.io/jwt/claims will be used. + ``claims_namespace`` i.e. https://hasura.io/jwt/claims will be used. ``claims_format`` ^^^^^^^^^^^^^^^^^