CVE-2019-1020015: Insufficient Information (NVD-CWE-noinfo)
value: graphql-engine (aka Hasura GraphQL Engine) before 1.0.0-beta.3 mishandles the audience check while verifying JWT.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 7.5
Published: 2019-07-29T13:15Z
Repository: https://github.com/hasura/graphql-engine
Language: Other
Files changed: .circleci/test-server.sh, docs/graphql/manual/auth/authentication/jwt.rst, server/src-lib/Hasura/Server/Auth.hs, server/src-lib/Hasura/Server/Auth/JWT.hs, server/tests-py/test_jwt.py
Commit date: 2019-07-11 09:58:39 +0000
Commit message: Merge pull request from GHSA-2j98-fw5g-j43v

* fix bug in audience check while verifying JWT

  - previously the check was converting the audience type into a string
  and then comparing with the conf value. all audience types (as it is a
  string or URI) will convert to plain strings
  - use the Audience type from the jose library for comparing

* add docs for audience

* add issuer check as well

* docs minor syntax fix

* skip audience check if not given in conf

* minor docs update

* qualify import jose library

Diff: diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh
index 5d65ef06..f1e41187 100755
--- a/.circleci/test-server.sh
+++ b/.circleci/test-server.sh
@@ -263,10 +263,54 @@ pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" -
 
 kill_hge_servers
 
 unset HASURA_GRAPHQL_JWT_SECRET
 
+echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with audience check - string) #####################################>\n"
+TEST_TYPE="jwt-audience-check-single-string"
+
+
+export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , audience: "myapp-1234"}')"
+
+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" test_jwt.py
+
+kill_hge_servers
+
+unset HASURA_GRAPHQL_JWT_SECRET
+
+echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with audience check - list of strings) #################################>\n"
+TEST_TYPE="jwt-audience-check-list-string"
+
+export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , audience: ["myapp-1234", "myapp-9876"]}')"
+
+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" test_jwt.py
+
+kill_hge_servers
+
+unset HASURA_GRAPHQL_JWT_SECRET
+
+echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with issuer check) #####################################>\n"
+TEST_TYPE="jwt-issuer-check"
+
+export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , issuer: "https://hasura.com"}')"
+
+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" test_jwt.py
+
+kill_hge_servers
+
+unset HASURA_GRAPHQL_JWT_SECRET
+
+
 # test with CORS modes
 
 echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH CORS DOMAINS ########>\n"
 export HASURA_GRAPHQL_CORS_DOMAIN="http://*.localhost, http://localhost:3000, https://*.foo.bar.com"
 TEST_TYPE="cors-domains"
diff --git a/docs/graphql/manual/auth/authentication/jwt.rst b/docs/graphql/manual/auth/authentication/jwt.rst
index 9d286b0e..d71a7794 100644
--- a/docs/graphql/manual/auth/authentication/jwt.rst
+++ b/docs/graphql/manual/auth/authentication/jwt.rst
@@ -228,11 +228,10 @@ If ``claims_format`` is ``stringified_json`` then JWT claims should look like:
     "admin": true,
     "iat": 1516239022,
     "https://hasura.io/jwt/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\"}"
   }
 
-
 ``audience``
 ^^^^^^^^^^^^
 This is an optional field. Certain providers might set a claim which indicates
 the intended audience for the JWT. This can be checked by setting this field.
 
@@ -250,22 +249,20 @@ Examples:
 
    {
      "type": "RS512",
      "jwk_url": "https://......",
      "audience": "myapp-1234"
-     ...
    }
 
 or
 
 .. code-block:: json
 
    {
      "type": "RS512",
      "jwk_url": "https://......",
      "audience": ["myapp-1234", "myapp-6789"]
-     ...
    }
 
 
 ``issuer``
 ^^^^^^^^^^
@@ -283,14 +280,14 @@ Examples:
 
    {
      "type": "RS512",
      "jwk_url": "https://......",
      "issuer": "https://my-auth-server.com"
-     ...
    }
 
 
+
 Examples
 ^^^^^^^^
 
 HMAC-SHA based
 ++++++++++++++
diff --git a/server/src-lib/Hasura/Server/Auth.hs b/server/src-lib/Hasura/Server/Auth.hs
index 4b0a1fa9..7dfc159e 100644
--- a/server/src-lib/Hasura/Server/Auth.hs
+++ b/server/src-lib/Hasura/Server/Auth.hs
@@ -127,11 +127,11 @@ mkJwtCtx conf httpManager loggerCtx = do
         Nothing -> return ref
         Just t -> do
           jwkRefreshCtrl logger httpManager url ref t
           return ref
   let claimsFmt = fromMaybe JCFJson (jcClaimsFormat conf)
-  return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf) claimsFmt
+  return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf) claimsFmt (jcIssuer conf)
 
 mkUserInfoFromResp
   :: (MonadIO m, MonadError QErr m)
   => L.Logger
   -> T.Text
diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs
index 4248bdbd..5032a56a 100644
--- a/server/src-lib/Hasura/Server/Auth/JWT.hs
+++ b/server/src-lib/Hasura/Server/Auth/JWT.hs
@@ -1,22 +1,21 @@
 module Hasura.Server.Auth.JWT
   ( processJwt
   , RawJWT
   , JWTConfig (..)
   , JWTCtx (..)
-  , JWKSet (..)
+  , Jose.JWKSet (..)
   , JWTClaimsFormat (..)
   , updateJwkRef
   , jwkRefreshCtrl
   , defaultClaimNs
   ) where
 
 import           Control.Arrow                   (first)
 import           Control.Exception               (try)
 import           Control.Lens
 import           Control.Monad                   (when)
-import           Crypto.JWT
 import           Data.IORef                      (IORef, modifyIORef, readIORef)
 
 import           Data.List                       (find)
 import           Data.Time.Clock                 (NominalDiffTime, UTCTime,
                                                   diffUTCTime, getCurrentTime)
@@ -31,10 +30,11 @@ import           Hasura.Server.Auth.JWT.Internal (parseHmacKey, parseRsaKey)
 import           Hasura.Server.Auth.JWT.Logging
 import           Hasura.Server.Utils             (diffTimeToMicro,
                                                   userRoleHeader)
 
 import qualified Control.Concurrent              as C
+import qualified Crypto.JWT                      as Jose
 import qualified Data.Aeson                      as A
 import qualified Data.Aeson.Casing               as A
 import qualified Data.Aeson.TH                   as A
 import qualified Data.ByteString.Lazy            as BL
 import qualified Data.ByteString.Lazy.Char8      as BLC
@@ -60,28 +60,29 @@ $(A.deriveJSON A.defaultOptions { A.sumEncoding = A.ObjectWithSingleField
                                 } ''JWTClaimsFormat)
 
 data JWTConfig
   = JWTConfig
   { jcType         :: !T.Text
-  , jcKeyOrUrl     :: !(Either JWK URI)
+  , jcKeyOrUrl     :: !(Either Jose.JWK URI)
   , jcClaimNs      :: !(Maybe T.Text)
-  , jcAudience     :: !(Maybe T.Text)
+  , jcAudience     :: !(Maybe Jose.Audience)
   , jcClaimsFormat :: !(Maybe JWTClaimsFormat)
-  -- , jcIssuer   :: !(Maybe T.Text)
+  , jcIssuer       :: !(Maybe Jose.StringOrURI)
   } deriving (Show, Eq)
 
 data JWTCtx
   = JWTCtx
-  { jcxKey          :: !(IORef JWKSet)
+  { jcxKey          :: !(IORef Jose.JWKSet)
   , jcxClaimNs      :: !(Maybe T.Text)
-  , jcxAudience     :: !(Maybe T.Text)
+  , jcxAudience     :: !(Maybe Jose.Audience)
   , jcxClaimsFormat :: !JWTClaimsFormat
+  , jcxIssuer       :: !(Maybe Jose.StringOrURI)
   } deriving (Eq)
 
 instance Show JWTCtx where
-  show (JWTCtx _ nsM audM cf) =
-    show ["<IORef JWKSet>", show nsM, show audM, show cf]
+  show (JWTCtx _ nsM audM cf iss) =
+    show ["<IORef JWKSet>", show nsM, show audM, show cf, show iss]
 
 data HasuraClaims
   = HasuraClaims
   { _cmAllowedRoles :: ![RoleName]
   , _cmDefaultRole  :: !RoleName
@@ -101,11 +102,11 @@ defaultClaimNs = "https://hasura.io/jwt/claims"
 jwkRefreshCtrl
   :: (MonadIO m)
   => Logger
   -> HTTP.Manager
   -> URI
-  -> IORef JWKSet
+  -> IORef Jose.JWKSet
   -> NominalDiffTime
   -> m ()
 jwkRefreshCtrl lggr mngr url ref time =
   void $ liftIO $ C.forkIO $ do
     C.threadDelay $ diffTimeToMicro time
@@ -122,11 +123,11 @@ updateJwkRef
   :: ( MonadIO m
      , MonadError T.Text m)
   => Logger
   -> HTTP.Manager
   -> URI
-  -> IORef JWKSet
+  -> IORef Jose.JWKSet
   -> m (Maybe NominalDiffTime)
 updateJwkRef (Logger logger) manager url jwkRef = do
   let options = wreqOptions manager []
       urlT    = T.pack $ show url
       infoMsg = "refreshing JWK from endpoint: " <> urlT
@@ -208,14 +209,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
-      expTimeM = fmap (\(NumericDate t) -> t) $ claims ^. claimExp
+      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 ^. unregisteredClaims
+  let mHasuraClaims = Map.lookup claimsNs $ claims ^. Jose.unregisteredClaims
   hasuraClaimsV <- maybe claimsNotFound return mHasuraClaims
 
   -- get hasura claims value as an object. parse from string possibly
   hasuraClaims <- parseObjectFromString claimsFmt hasuraClaimsV
 
@@ -320,36 +321,44 @@ parseHasuraClaims claimsMap = do
         A.Error e     -> throw400 JWTInvalidClaims $ errFn e
 
 
 -- | Verify the JWT against given JWK
 verifyJwt
-  :: ( MonadError JWTError m
+  :: ( MonadError Jose.JWTError m
      , MonadIO m
      )
   => JWTCtx
   -> RawJWT
-  -> m ClaimsSet
+  -> m Jose.ClaimsSet
 verifyJwt ctx (RawJWT rawJWT) = do
   key <- liftIO $ readIORef $ jcxKey ctx
-  jwt <- decodeCompact rawJWT
+  jwt <- Jose.decodeCompact rawJWT
   t   <- liftIO getCurrentTime
-  verifyClaimsAt config key t jwt
+  Jose.verifyClaimsAt config key t jwt
   where
-    audCheck aud = maybe True (== (T.pack . show) aud) $ jcxAudience ctx
-    config = defaultJWTValidationSettings audCheck
+    config = case jcxIssuer ctx of
+      Nothing  -> Jose.defaultJWTValidationSettings audCheck
+      Just iss -> Jose.defaultJWTValidationSettings audCheck
+                  & set Jose.issuerPredicate (== iss)
+    audCheck audience =
+      -- dont perform the check if there are no audiences in the conf
+      case jcxAudience ctx of
+        Nothing                        -> True
+        Just (Jose.Audience audiences) -> audience `elem` audiences
 
 
 instance A.ToJSON JWTConfig where
-  toJSON (JWTConfig ty keyOrUrl claimNs aud claimsFmt) =
+  toJSON (JWTConfig ty keyOrUrl claimNs aud claimsFmt iss) =
     case keyOrUrl of
          Left _    -> mkObj ("key" A..= A.String "<JWK REDACTED>")
          Right url -> mkObj ("jwk_url" A..= url)
     where
       mkObj item = A.object [ "type" A..= ty
                             , "claims_namespace" A..= claimNs
                             , "claims_format" A..= claimsFmt
                             , "audience" A..= aud
+                            , "issuer" A..= iss
                             , item
                             ]
 
 -- | Parse from a json string like:
 -- | `{"type": "RS256", "key": "<PEM-encoded-public-key-or-X509-cert>"}`
@@ -359,21 +368,22 @@ instance A.FromJSON JWTConfig where
   parseJSON = A.withObject "JWTConfig" $ \o -> do
     keyType <- o A..: "type"
     mRawKey <- o A..:? "key"
     claimNs <- o A..:? "claims_namespace"
     aud     <- o A..:? "audience"
+    iss     <- o A..:? "issuer"
     jwkUrl  <- o A..:? "jwk_url"
     isStrngfd <- o A..:? "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
+        return $ JWTConfig keyType (Left key) claimNs aud isStrngfd iss
       (Nothing, Just url) ->
-        return $ JWTConfig keyType (Right url) claimNs aud isStrngfd
+        return $ JWTConfig keyType (Right url) claimNs aud isStrngfd iss
 
     where
       parseKey keyType rawKey =
        case keyType of
           "HS256" -> runEither $ parseHmacKey rawKey 256
diff --git a/server/tests-py/test_jwt.py b/server/tests-py/test_jwt.py
index c61a62fb..78eef18a 100644
--- a/server/tests-py/test_jwt.py
+++ b/server/tests-py/test_jwt.py
@@ -13,14 +13,14 @@ from cryptography.hazmat.primitives import serialization
 
 from validate import check_query
 
 
 if not pytest.config.getoption('--hge-jwt-key-file'):
-    pytest.skip('--hge-jwt-key-file is missing, skipping JWT basic tests', allow_module_level=True)
+    pytest.skip('--hge-jwt-key-file is missing, skipping JWT tests', allow_module_level=True)
 
 if not pytest.config.getoption('--hge-jwt-conf'):
-    pytest.skip('--hge-jwt-key-conf is missing, skipping JWT basic tests', allow_module_level=True)
+    pytest.skip('--hge-jwt-key-conf is missing, skipping JWT tests', allow_module_level=True)
 
 def get_claims_fmt(raw_conf):
     conf = json.loads(raw_conf)
     try:
         claims_fmt = conf['claims_format']
@@ -197,10 +197,36 @@ class TestJWTBasic():
             self.conf['status'] = 200
         if endpoint == '/v1alpha1/graphql':
             self.conf['status'] = 400
         check_query(hge_ctx, self.conf, add_auth=False)
 
+    def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint):
+        self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
+            'x-hasura-user-id': '1',
+            'x-hasura-default-role': 'user',
+            'x-hasura-allowed-roles': ['user'],
+        })
+
+        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)
+
+    def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint):
+        self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
+            'x-hasura-user-id': '1',
+            'x-hasura-default-role': 'user',
+            'x-hasura-allowed-roles': ['user'],
+        })
+
+        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)
+
     @pytest.fixture(autouse=True)
     def transact(self, setup):
         self.dir = 'queries/graphql_query/permissions'
         with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c:
             self.conf = yaml.safe_load(c)
@@ -259,5 +285,151 @@ class TestSubscriptionJwtExpiry(object):
             }
         }
         init_ws_conn(hge_ctx, ws_client, payload)
         time.sleep(5)
         assert ws_client.remote_closed == True, ws_client.remote_closed
+
+
+@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
+class TestJwtAudienceCheck():
+    def test_jwt_valid_audience(self, hge_ctx, endpoint):
+        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
+        if 'audience' not in jwt_conf:
+            pytest.skip('audience not present in conf, skipping testing audience')
+
+        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, {
+            'x-hasura-user-id': '1',
+            'x-hasura-default-role': 'user',
+            'x-hasura-allowed-roles': ['user'],
+        })
+        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)
+
+    def test_jwt_invalid_audience(self, hge_ctx, endpoint):
+        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
+        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, {
+            'x-hasura-user-id': '1',
+            'x-hasura-default-role': 'user',
+            'x-hasura-allowed-roles': ['user'],
+        })
+        self.claims['aud'] = 'rubbish_audience'
+
+        token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
+        self.conf['headers']['Authorization'] = 'Bearer ' + token
+        self.conf['response'] = {
+            'errors': [{
+                'extensions': {
+                    'code': 'invalid-jwt',
+                    'path': '$'
+                },
+                'message': 'Could not verify JWT: JWTNotInAudience'
+            }]
+        }
+        self.conf['url'] = endpoint
+        if endpoint == '/v1/graphql':
+            self.conf['status'] = 200
+        if endpoint == '/v1alpha1/graphql':
+            self.conf['status'] = 400
+        check_query(hge_ctx, self.conf, add_auth=False)
+
+    @pytest.fixture(autouse=True)
+    def transact(self, setup):
+        self.dir = 'queries/graphql_query/permissions'
+        with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c:
+            self.conf = yaml.safe_load(c)
+        curr_time = datetime.now()
+        exp_time = curr_time + timedelta(hours=1)
+        self.claims = {
+            'sub': '1234567890',
+            'name': 'John Doe',
+            'iat': math.floor(curr_time.timestamp()),
+            'exp': math.floor(exp_time.timestamp())
+        }
+
+    @pytest.fixture(scope='class')
+    def setup(self, request, hge_ctx):
+        self.dir = 'queries/graphql_query/permissions'
+        st_code, resp = hge_ctx.v1q_f(self.dir + '/setup.yaml')
+        assert st_code == 200, resp
+        yield
+        st_code, resp = hge_ctx.v1q_f(self.dir + '/teardown.yaml')
+        assert st_code == 200, resp
+
+@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
+class TestJwtIssuerCheck():
+    def test_jwt_valid_issuer(self, hge_ctx, endpoint):
+        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
+        if 'issuer' not in jwt_conf:
+            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, {
+            'x-hasura-user-id': '1',
+            'x-hasura-default-role': 'user',
+            'x-hasura-allowed-roles': ['user'],
+        })
+        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)
+
+    def test_jwt_invalid_issuer(self, hge_ctx, endpoint):
+        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
+        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, {
+            'x-hasura-user-id': '1',
+            'x-hasura-default-role': 'user',
+            'x-hasura-allowed-roles': ['user'],
+        })
+        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['response'] = {
+            'errors': [{
+                'extensions': {
+                    'code': 'invalid-jwt',
+                    'path': '$'
+                },
+                'message': 'Could not verify JWT: JWTNotInIssuer'
+            }]
+        }
+        self.conf['url'] = endpoint
+        if endpoint == '/v1/graphql':
+            self.conf['status'] = 200
+        if endpoint == '/v1alpha1/graphql':
+            self.conf['status'] = 400
+        check_query(hge_ctx, self.conf, add_auth=False)
+
+    @pytest.fixture(autouse=True)
+    def transact(self, setup):
+        self.dir = 'queries/graphql_query/permissions'
+        with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c:
+            self.conf = yaml.safe_load(c)
+        curr_time = datetime.now()
+        exp_time = curr_time + timedelta(hours=1)
+        self.claims = {
+            'sub': '1234567890',
+            'name': 'John Doe',
+            'iat': math.floor(curr_time.timestamp()),
+            'exp': math.floor(exp_time.timestamp())
+        }
+
+    @pytest.fixture(scope='class')
+    def setup(self, request, hge_ctx):
+        self.dir = 'queries/graphql_query/permissions'
+        st_code, resp = hge_ctx.v1q_f(self.dir + '/setup.yaml')
+        assert st_code == 200, resp
+        yield
+        st_code, resp = hge_ctx.v1q_f(self.dir + '/teardown.yaml')
+        assert st_code == 200, resp

Vulnerable code: 
     ...
     ...
     ...
  return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf) claimsFmt
  , JWKSet (..)
import           Crypto.JWT
  , jcKeyOrUrl     :: !(Either JWK URI)
  , jcAudience     :: !(Maybe T.Text)
  -- , jcIssuer   :: !(Maybe T.Text)
  { jcxKey          :: !(IORef JWKSet)
  , jcxAudience     :: !(Maybe T.Text)
  show (JWTCtx _ nsM audM cf) =
    show ["<IORef JWKSet>", show nsM, show audM, show cf]
  -> IORef JWKSet
  -> IORef JWKSet
      expTimeM = fmap (\(NumericDate t) -> t) $ claims ^. claimExp
  let mHasuraClaims = Map.lookup claimsNs $ claims ^. unregisteredClaims
  :: ( MonadError JWTError m
  -> m ClaimsSet
  jwt <- decodeCompact rawJWT
  verifyClaimsAt config key t jwt
    audCheck aud = maybe True (== (T.pack . show) aud) $ jcxAudience ctx
    config = defaultJWTValidationSettings audCheck
  toJSON (JWTConfig ty keyOrUrl claimNs aud claimsFmt) =
        return $ JWTConfig keyType (Left key) claimNs aud isStrngfd
        return $ JWTConfig keyType (Right url) claimNs aud isStrngfd
    pytest.skip('--hge-jwt-key-file is missing, skipping JWT basic tests', allow_module_level=True)
    pytest.skip('--hge-jwt-key-conf is missing, skipping JWT basic tests', allow_module_level=True)
Fixed code: echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with audience check - string) #####################################>\n"
TEST_TYPE="jwt-audience-check-single-string"


export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , audience: "myapp-1234"}')"

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" test_jwt.py

kill_hge_servers

unset HASURA_GRAPHQL_JWT_SECRET

echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with audience check - list of strings) #################################>\n"
TEST_TYPE="jwt-audience-check-list-string"

export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , audience: ["myapp-1234", "myapp-9876"]}')"

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" test_jwt.py

kill_hge_servers

unset HASURA_GRAPHQL_JWT_SECRET

echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with issuer check) #####################################>\n"
TEST_TYPE="jwt-issuer-check"

export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , issuer: "https://hasura.com"}')"

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" test_jwt.py

kill_hge_servers

unset HASURA_GRAPHQL_JWT_SECRET



  return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf) claimsFmt (jcIssuer conf)
  , Jose.JWKSet (..)
import qualified Crypto.JWT                      as Jose
  , jcKeyOrUrl     :: !(Either Jose.JWK URI)
  , jcAudience     :: !(Maybe Jose.Audience)
  , jcIssuer       :: !(Maybe Jose.StringOrURI)
  { jcxKey          :: !(IORef Jose.JWKSet)
  , jcxAudience     :: !(Maybe Jose.Audience)
  , jcxIssuer       :: !(Maybe Jose.StringOrURI)
  show (JWTCtx _ nsM audM cf iss) =
    show ["<IORef JWKSet>", show nsM, show audM, show cf, show iss]
  -> IORef Jose.JWKSet
  -> IORef Jose.JWKSet
      expTimeM = fmap (\(Jose.NumericDate t) -> t) $ claims ^. Jose.claimExp
  let mHasuraClaims = Map.lookup claimsNs $ claims ^. Jose.unregisteredClaims
  :: ( MonadError Jose.JWTError m
  -> m Jose.ClaimsSet
  jwt <- Jose.decodeCompact rawJWT
  Jose.verifyClaimsAt config key t jwt
    config = case jcxIssuer ctx of
      Nothing  -> Jose.defaultJWTValidationSettings audCheck
      Just iss -> Jose.defaultJWTValidationSettings audCheck
                  & set Jose.issuerPredicate (== iss)
    audCheck audience =
      -- dont perform the check if there are no audiences in the conf
      case jcxAudience ctx of
        Nothing                        -> True
        Just (Jose.Audience audiences) -> audience `elem` audiences
  toJSON (JWTConfig ty keyOrUrl claimNs aud claimsFmt iss) =
                            , "issuer" A..= iss
    iss     <- o A..:? "issuer"
        return $ JWTConfig keyType (Left key) claimNs aud isStrngfd iss
        return $ JWTConfig keyType (Right url) claimNs aud isStrngfd iss
    pytest.skip('--hge-jwt-key-file is missing, skipping JWT tests', allow_module_level=True)
    pytest.skip('--hge-jwt-key-conf is missing, skipping JWT tests', allow_module_level=True)
    def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint):
        self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
            'x-hasura-user-id': '1',
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
        })

        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)

    def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint):
        self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
            'x-hasura-user-id': '1',
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
        })

        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)



@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
class TestJwtAudienceCheck():
    def test_jwt_valid_audience(self, hge_ctx, endpoint):
        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
        if 'audience' not in jwt_conf:
            pytest.skip('audience not present in conf, skipping testing audience')

        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, {
            'x-hasura-user-id': '1',
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
        })
        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)

    def test_jwt_invalid_audience(self, hge_ctx, endpoint):
        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
        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, {
            'x-hasura-user-id': '1',
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
        })
        self.claims['aud'] = 'rubbish_audience'

        token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
        self.conf['headers']['Authorization'] = 'Bearer ' + token
        self.conf['response'] = {
            'errors': [{
                'extensions': {
                    'code': 'invalid-jwt',
                    'path': '$'
                },
                'message': 'Could not verify JWT: JWTNotInAudience'
            }]
        }
        self.conf['url'] = endpoint
        if endpoint == '/v1/graphql':
            self.conf['status'] = 200
        if endpoint == '/v1alpha1/graphql':
            self.conf['status'] = 400
        check_query(hge_ctx, self.conf, add_auth=False)

    @pytest.fixture(autouse=True)
    def transact(self, setup):
        self.dir = 'queries/graphql_query/permissions'
        with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c:
            self.conf = yaml.safe_load(c)
        curr_time = datetime.now()
        exp_time = curr_time + timedelta(hours=1)
        self.claims = {
            'sub': '1234567890',
            'name': 'John Doe',
            'iat': math.floor(curr_time.timestamp()),
            'exp': math.floor(exp_time.timestamp())
        }

    @pytest.fixture(scope='class')
    def setup(self, request, hge_ctx):
        self.dir = 'queries/graphql_query/permissions'
        st_code, resp = hge_ctx.v1q_f(self.dir + '/setup.yaml')
        assert st_code == 200, resp
        yield
        st_code, resp = hge_ctx.v1q_f(self.dir + '/teardown.yaml')
        assert st_code == 200, resp

@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
class TestJwtIssuerCheck():
    def test_jwt_valid_issuer(self, hge_ctx, endpoint):
        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
        if 'issuer' not in jwt_conf:
            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, {
            'x-hasura-user-id': '1',
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
        })
        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)

    def test_jwt_invalid_issuer(self, hge_ctx, endpoint):
        jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
        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, {
            'x-hasura-user-id': '1',
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
        })
        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['response'] = {
            'errors': [{
                'extensions': {
                    'code': 'invalid-jwt',
                    'path': '$'
                },
                'message': 'Could not verify JWT: JWTNotInIssuer'
            }]
        }
        self.conf['url'] = endpoint
        if endpoint == '/v1/graphql':
            self.conf['status'] = 200
        if endpoint == '/v1alpha1/graphql':
            self.conf['status'] = 400
        check_query(hge_ctx, self.conf, add_auth=False)

    @pytest.fixture(autouse=True)
    def transact(self, setup):
        self.dir = 'queries/graphql_query/permissions'
        with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c:
            self.conf = yaml.safe_load(c)
        curr_time = datetime.now()
        exp_time = curr_time + timedelta(hours=1)
        self.claims = {
            'sub': '1234567890',
            'name': 'John Doe',
            'iat': math.floor(curr_time.timestamp()),
            'exp': math.floor(exp_time.timestamp())
        }

    @pytest.fixture(scope='class')
    def setup(self, request, hge_ctx):
        self.dir = 'queries/graphql_query/permissions'
        st_code, resp = hge_ctx.v1q_f(self.dir + '/setup.yaml')
        assert st_code == 200, resp
        yield
        st_code, resp = hge_ctx.v1q_f(self.dir + '/teardown.yaml')
        assert st_code == 200, resp