这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .circleci/test-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,21 @@ export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jw

"$GRAPHQL_ENGINE" serve >> "$OUTPUT_FOLDER/graphql-engine.log" & PID=$!

pytest -vv --hge-url="$HGE_URL" --pg-url="$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ACCESS_KEY" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key"
pytest -vv --hge-url="$HGE_URL" --pg-url="$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ACCESS_KEY" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET"

kill -INT $PID
sleep 4
combine_hpc_reports

unset HASURA_GRAPHQL_JWT_SECRET

echo -e "\n<########## TEST GRAPHQL-ENGINE WITH ACCESS KEY AND JWT (in stringified mode) #####################################>\n"

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

"$GRAPHQL_ENGINE" serve >> "$OUTPUT_FOLDER/graphql-engine.log" & PID=$!

pytest -vv --hge-url="$HGE_URL" --pg-url="$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ACCESS_KEY" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" test_jwt.py

kill -INT $PID
sleep 4
Expand Down
54 changes: 52 additions & 2 deletions docs/graphql/manual/auth/jwt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ JSON object:
"type": "<standard-JWT-algorithms>",
"key": "<optional-key-as-string>",
"jwk_url": "<optional-url-to-refresh-jwks>",
"claims_namespace": "<optional-key-name-in-claims>"
"claims_namespace": "<optional-key-name-in-claims>",
"claims_format": "json|stringified_json"
}

``key`` or ``jwk_url``, either of them has to be present.
``key`` or ``jwk_url``, **one of them has to be present**.

``type``
^^^^^^^^
Expand Down Expand Up @@ -174,6 +175,55 @@ inside which the Hasura specific claims will be present. E.g. - ``https://mydoma

**Default value** is: ``https://hasura.io/jwt/claims``.


``claims_format``
^^^^^^^^^^^^^^^^^^
This is an optional field, with only the following possible values:
- ``json``
- ``stringified_json``

Default is ``json``.

This is to indicate that if the hasura specific claims are a regular JSON object
or stringified JSON

This is required because providers like AWS Cognito only allows strings in the
JWT claims. `See #1176 <https://github.com/hasura/graphql-engine/issues/1176>`_.

Example:-

If ``claims_format`` is ``json`` then JWT claims should look like:

.. code-block:: json

{
"sub": "1234567890",
"name": "John Doe",
"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"
}
}


If ``claims_format`` is ``stringified_json`` then JWT claims should look like:

.. code-block:: json

{
"sub": "1234567890",
"name": "John Doe",
"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\"}"
}


Examples
^^^^^^^^

Expand Down
3 changes: 2 additions & 1 deletion server/src-lib/Hasura/Server/Auth.hs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ mkJwtCtx jwtConf httpManager loggerCtx = do
Just t -> do
jwkRefreshCtrl logger httpManager url ref t
return ref
return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf)
let claimsFmt = fromMaybe JCFJson (jcClaimsFormat conf)
return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf) claimsFmt
where
decodeErr e = throwError . T.pack $ "Fatal Error: JWT conf: " <> e

Expand Down
64 changes: 46 additions & 18 deletions server/src-lib/Hasura/Server/Auth/JWT.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Hasura.Server.Auth.JWT
, JWTConfig (..)
, JWTCtx (..)
, JWKSet (..)
, JWTClaimsFormat (..)
, updateJwkRef
, jwkRefreshCtrl
) where
Expand Down Expand Up @@ -39,32 +40,44 @@ import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as Map
import qualified Data.String.Conversions as CS
import qualified Data.Text as T
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


newtype RawJWT = RawJWT BL.ByteString

data JWTClaimsFormat
= JCFJson
| JCFStringifiedJson
deriving (Show, Eq)

$(A.deriveJSON A.defaultOptions { A.sumEncoding = A.ObjectWithSingleField
, A.constructorTagModifier = A.snakeCase . drop 3
} ''JWTClaimsFormat)

data JWTConfig
= JWTConfig
{ jcType :: !T.Text
, jcKeyOrUrl :: !(Either JWK URI)
, jcClaimNs :: !(Maybe T.Text)
, jcAudience :: !(Maybe T.Text)
{ jcType :: !T.Text
, jcKeyOrUrl :: !(Either JWK URI)
, jcClaimNs :: !(Maybe T.Text)
, jcAudience :: !(Maybe T.Text)
, jcClaimsFormat :: !(Maybe JWTClaimsFormat)
-- , jcIssuer :: !(Maybe T.Text)
} deriving (Show, Eq)

data JWTCtx
= JWTCtx
{ jcxKey :: !(IORef JWKSet)
, jcxClaimNs :: !(Maybe T.Text)
, jcxAudience :: !(Maybe T.Text)
{ jcxKey :: !(IORef JWKSet)
, jcxClaimNs :: !(Maybe T.Text)
, jcxAudience :: !(Maybe T.Text)
, jcxClaimsFormat :: !JWTClaimsFormat
} deriving (Eq)

instance Show JWTCtx where
show (JWTCtx _ nsM audM) =
show ["<IORef JWKSet>", show nsM, show audM]
show (JWTCtx _ nsM audM cf) =
show ["<IORef JWKSet>", show nsM, show audM, show cf]

data HasuraClaims
= HasuraClaims
Expand Down Expand Up @@ -188,13 +201,15 @@ processAuthZHeader jwtCtx headers authzHeader = do
-- verify the JWT
claims <- liftJWTError invalidJWTError $ verifyJwt jwtCtx $ RawJWT jwt

let claimsNs = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx
let claimsNs = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx
claimsFmt = jcxClaimsFormat jwtCtx

-- see if the hasura claims key exist in the claims map
let mHasuraClaims = Map.lookup claimsNs $ claims ^. unregisteredClaims
hasuraClaimsV <- maybe claimsNotFound return mHasuraClaims
-- the value of hasura claims key has to be an object
hasuraClaims <- validateIsObject hasuraClaimsV

-- get hasura claims value as an object. parse from string possibly
hasuraClaims <- parseObjectFromString claimsFmt hasuraClaimsV

-- filter only x-hasura claims and convert to lower-case
let claimsMap = Map.filterWithKey (\k _ -> T.isPrefixOf "x-hasura-" k)
Expand All @@ -220,10 +235,22 @@ processAuthZHeader jwtCtx headers authzHeader = do
["Bearer", jwt] -> return jwt
_ -> malformedAuthzHeader

validateIsObject jVal =
case jVal of
A.Object x -> return x
_ -> throw400 JWTInvalidClaims "hasura claims should be an object"
parseObjectFromString claimsFmt jVal =
case (claimsFmt, jVal) of
(JCFStringifiedJson, A.String v) ->
either (const $ claimsErr $ strngfyErr v) return
$ A.eitherDecodeStrict $ T.encodeUtf8 v
(JCFStringifiedJson, _) ->
claimsErr "expecting a string when claims_format is stringified_json"
(JCFJson, A.Object o) -> return o
(JCFJson, _) ->
claimsErr "expecting a json object when claims_format is json"

strngfyErr v = "expecting stringified json at: '"
<> fromMaybe defaultClaimNs (jcxClaimNs jwtCtx)
<> "', but found: " <> v

claimsErr = throw400 JWTInvalidClaims

-- see if there is a x-hasura-role header, or else pick the default role
getCurrentRole defaultRole =
Expand Down Expand Up @@ -314,15 +341,16 @@ instance A.FromJSON JWTConfig where
claimNs <- o A..:? "claims_namespace"
aud <- o A..:? "audience"
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
return $ JWTConfig keyType (Left key) claimNs aud isStrngfd
(Nothing, Just url) ->
return $ JWTConfig keyType (Right url) claimNs aud
return $ JWTConfig keyType (Right url) claimNs aud isStrngfd

where
parseKey keyType rawKey =
Expand Down
14 changes: 13 additions & 1 deletion server/tests-py/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def pytest_addoption(parser):
parser.addoption(
"--hge-jwt-key-file", metavar="HGE_JWT_KEY_FILE", help="File containting the private key used to encode jwt tokens using RS512 algorithm", required=False
)
parser.addoption(
"--hge-jwt-conf", metavar="HGE_JWT_CONF", help="The JWT conf", required=False
)


@pytest.fixture(scope='session')
Expand All @@ -34,8 +37,17 @@ def hge_ctx(request):
hge_webhook = request.config.getoption('--hge-webhook')
webhook_insecure = request.config.getoption('--test-webhook-insecure')
hge_jwt_key_file = request.config.getoption('--hge-jwt-key-file')
hge_jwt_conf = request.config.getoption('--hge-jwt-conf')
try:
hge_ctx = HGECtx(hge_url=hge_url, pg_url=pg_url, hge_key=hge_key, hge_webhook=hge_webhook, hge_jwt_key_file=hge_jwt_key_file, webhook_insecure = webhook_insecure )
hge_ctx = HGECtx(
hge_url=hge_url,
pg_url=pg_url,
hge_key=hge_key,
hge_webhook=hge_webhook,
webhook_insecure=webhook_insecure,
hge_jwt_key_file=hge_jwt_key_file,
hge_jwt_conf=hge_jwt_conf
)
except HGECtxError as e:
pytest.exit(str(e))
yield hge_ctx # provide the fixture value
Expand Down
3 changes: 2 additions & 1 deletion server/tests-py/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def server_bind(self):


class HGECtx:
def __init__(self, hge_url, pg_url, hge_key, hge_webhook, hge_jwt_key_file, webhook_insecure):
def __init__(self, hge_url, pg_url, hge_key, hge_webhook, webhook_insecure, hge_jwt_key_file, hge_jwt_conf):
server_address = ('0.0.0.0', 5592)

self.resp_queue = queue.Queue(maxsize=1)
Expand All @@ -83,6 +83,7 @@ def __init__(self, hge_url, pg_url, hge_key, hge_webhook, hge_jwt_key_file, webh
else:
with open(hge_jwt_key_file) as f:
self.hge_jwt_key = f.read()
self.hge_jwt_conf = hge_jwt_conf
self.webhook_insecure = webhook_insecure
self.may_skip_test_teardown = False

Expand Down
Loading