server: configuration to specify which Header to pick for JWT Authentication

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9942
GitOrigin-RevId: 4f18d29f07c213a001d43b88e3ce2991230e28e6
This commit is contained in:
pranshi06 2023-08-04 16:05:56 +05:30 committed by hasura-bot
parent c12c8e974c
commit 7647456718
3 changed files with 42 additions and 5 deletions

View File

@ -542,6 +542,7 @@ The following are the possible values:
- `{"type": "Authorization"}`
- `{"type": "Cookie", "name": "cookie_name" }`
- `{"type": "CustomHeader", "name": "header_name" }`
Default is `{"type": "Authorization"}`.
@ -550,6 +551,8 @@ In the default mode, Hasura expects an `Authorization` header with a `Bearer` to
In the cookie mode, Hasura will try to parse the cookie header with the given cookie name. The value of the cookie
should be the exact JWT.
In the custom header mode, Hasura expects a `header_name` header with the exact JWT token value.
Example:
If `header` is `{"type": "Authorization"}` then JWT header should look like:
@ -564,6 +567,12 @@ If `header` is `{"type": "Cookie", "name": "cookie_name" }` then JWT header shou
Cookie: cookie_name=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI...
```
If `header` is `{"type": "CustomHeader", "name": "header_name" }` then JWT header should look like:
```none
header_name: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI...
```
## Run Hasura GraphQL Engine in JWT mode {#running-with-jwt}
### Docker

View File

@ -138,6 +138,7 @@ instance J.ToJSON JWTClaimsFormat where
data JWTHeader
= JHAuthorization
| JHCookie Text -- cookie name
| JHCustomHeader Text -- header name
deriving (Show, Eq, Generic)
instance Hashable JWTHeader
@ -148,7 +149,8 @@ instance J.FromJSON JWTHeader where
if
| hdrType == "Authorization" -> pure JHAuthorization
| hdrType == "Cookie" -> JHCookie <$> o J..: "name"
| otherwise -> fail "expected 'type' is 'Authorization' or 'Cookie'"
| hdrType == "CustomHeader" -> JHCustomHeader <$> o J..: "name"
| otherwise -> fail "expected 'type' is 'Authorization' or 'Cookie' or 'CustomHeader'"
instance J.ToJSON JWTHeader where
toJSON JHAuthorization = J.object ["type" J..= ("Authorization" :: String)]
@ -157,6 +159,11 @@ instance J.ToJSON JWTHeader where
[ "type" J..= ("Cookie" :: String),
"name" J..= name
]
toJSON (JHCustomHeader name) =
J.object
[ "type" J..= ("CustomHeader" :: String),
"name" J..= name
]
defaultClaimsFormat :: JWTClaimsFormat
defaultClaimsFormat = JCFJson
@ -537,6 +544,7 @@ processJwt_ processJwtBytes decodeIssuer fGetHeaderType jwtCtxs headers mUnAuthR
case BC.words b' of
["Bearer", jwt] -> pure jwt
_ -> throw400 InvalidHeaders "Malformed Authorization header"
(JHCustomHeader _, b') -> pure b'
case (StringOrURI <$> jcxIssuer j, decodeIssuer $ RawJWT $ BLC.fromStrict b'') of
(Nothing, _) -> pure $ Right (j, b'')
@ -551,13 +559,22 @@ processJwt_ processJwtBytes decodeIssuer fGetHeaderType jwtCtxs headers mUnAuthR
keyCtxOnAuthTypes :: [JWTCtx] -> HashMap.HashMap AuthTokenLocation [JWTCtx]
keyCtxOnAuthTypes = HashMap.fromListWith (++) . fmap (expectedHeader &&& pure)
keyTokensOnAuthTypes :: [HTTP.Header] -> HashMap.HashMap AuthTokenLocation [(AuthTokenLocation, B.ByteString)]
keyTokensOnAuthTypes = HashMap.fromListWith (++) . map (fst &&& pure) . concatMap findTokensInHeader
getCustomHeaderName :: JWTHeader -> Maybe Text
getCustomHeaderName = \case
JHCustomHeader headerName -> Just headerName
_ -> Nothing
findTokensInHeader :: Header -> [(AuthTokenLocation, B.ByteString)]
findTokensInHeader (key, val)
getCustomHeaderNameList = mapMaybe (getCustomHeaderName . fGetHeaderType) jwtCtxs
keyTokensOnAuthTypes :: [HTTP.Header] -> HashMap.HashMap AuthTokenLocation [(AuthTokenLocation, B.ByteString)]
keyTokensOnAuthTypes = HashMap.fromListWith (++) . map (fst &&& pure) . concatMap (findTokensInHeader getCustomHeaderNameList)
findTokensInHeader :: [Text] -> Header -> [(AuthTokenLocation, B.ByteString)]
findTokensInHeader jwtCustomHeaderList (key, val)
| key == CI.mk "Authorization" = [(JHAuthorization, val)]
| key == CI.mk "Cookie" = bimap JHCookie T.encodeUtf8 <$> Spock.parseCookies val
| key `elem` (map (CI.mk . T.encodeUtf8) jwtCustomHeaderList) =
[(JHCustomHeader (T.toLower $ T.decodeUtf8 $ CI.original key), val)]
| otherwise = []
expectedHeader :: JWTCtx -> AuthTokenLocation
@ -565,6 +582,7 @@ processJwt_ processJwtBytes decodeIssuer fGetHeaderType jwtCtxs headers mUnAuthR
case fGetHeaderType jwtCtx of
JHAuthorization -> JHAuthorization
JHCookie name -> JHCookie name
JHCustomHeader name -> JHCustomHeader (T.toLower name)
withAuthZ authzHeader jwtCtx = do
authMode <- processJwtBytes jwtCtx $ BL.fromStrict authzHeader

View File

@ -54,6 +54,8 @@ def get_header_fmt(conf):
return (hdr_fmt, None)
elif hdr_fmt == 'Cookie':
return (hdr_fmt, conf['header']['name'])
elif hdr_fmt == "CustomHeader":
return (hdr_fmt, conf['header']['name'])
else:
raise Exception('Invalid JWT header format: %s' % conf)
except KeyError:
@ -68,6 +70,8 @@ def mk_authz_header(conf, token):
return {'Authorization': 'Bearer ' + token}
elif header == 'Cookie' and name:
return {'Cookie': name + '=' + token}
elif header == 'CustomHeader' and name:
return {name: token}
else:
raise Exception('Invalid JWT header format')
@ -375,6 +379,12 @@ class TestJwtBasicWithEd25519AndCookie(AbstractTestJwtBasic):
class TestJwtBasicWithEsAndCookie(AbstractTestJwtBasic):
pass
@pytest.mark.jwt('rsa', {
'header': {'type': 'CustomHeader', 'name': 'hasura_user'},
})
class TestJwtBasicWithRsaAndCustomHeader(AbstractTestJwtBasic):
pass
@pytest.mark.jwt('rsa', {
'claims_format': 'stringified_json',