mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
Adding multiple jwt secrets (incorporating provenance requirements)
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3173 Co-authored-by: Solomon <24038+solomon-b@users.noreply.github.com> Co-authored-by: Daniel Chambers <1214352+daniel-chambers@users.noreply.github.com> GitOrigin-RevId: 395a5d5854896f866b612895d6f41e29376c2caa
This commit is contained in:
parent
47315b78d6
commit
ff6aac31b8
@ -23,7 +23,6 @@ The optimization can be enabled using the
|
||||
- console: enable support for update permissions for mssql #3591
|
||||
- server: add support for customization of table GraphQL schema descriptions (#7496)
|
||||
- cli: skip tls verfication for all API requests when `insecure-skip-tls-verify` flag is set (#4926)
|
||||
|
||||
- server: classify MSSQL exceptions and improve API error responses
|
||||
|
||||
## v2.2.0
|
||||
|
@ -31,3 +31,4 @@ Features
|
||||
api-limits
|
||||
disable-graphql-introspection
|
||||
rotating-admin-secrets
|
||||
multiple-jwt-secrets
|
||||
|
40
docs/graphql/cloud/security/mutiple-jwt-secrets.rst
Normal file
40
docs/graphql/cloud/security/mutiple-jwt-secrets.rst
Normal file
@ -0,0 +1,40 @@
|
||||
.. meta::
|
||||
:description: Hasura Cloud multiple JWT Secrets
|
||||
:keywords: hasura, docs, cloud, security, allow, , multiple, JWT, secrets
|
||||
|
||||
.. _multiple_jwt_secrets:
|
||||
|
||||
Multiple JWT Secrets
|
||||
===========
|
||||
|
||||
.. contents:: Table of contents
|
||||
:backlinks: none
|
||||
:depth: 1
|
||||
:local:
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
You can configure Hasura with a list of JWT Secrets so that you can integrate with different JWT issuers. This is useful when you have different authentication providers using the same Hasura infrastructure.
|
||||
|
||||
How to use multiple jwt secrets
|
||||
-------------------------------
|
||||
|
||||
Multiple JWT secrets can be provided in the env var ``HASURA_GRAPHQL_JWT_SECRETS`` which takes a JSON array of JWT secret objects.
|
||||
|
||||
Bearer Tokens are authenticated against the secret with a matching or absent `issuer`.
|
||||
|
||||
The authentication is resolved as follows:
|
||||
|
||||
1. Raw token values matched with configurations by looking in locations configured in HASURA_GRAPHQL_JWT_SECRETS under the "header" field (Authorization Header if missing).
|
||||
2. Tokens are filtered to ensure that the `issuer` field matches, or that the issuer is absent either from the configuration, or the token.
|
||||
3. If no candidate tokens are found then the unauthorized flow is performed (depends on ``HASURA_GRAPHQL_UNAUTHORIZED_ROLE``).
|
||||
3. If multiple candidate tokens are found then an error is raised as the desired token is ambiguous.
|
||||
3. If one candidate token is found then it is verified against the corresponding configured secret.
|
||||
|
||||
.. note::
|
||||
Authentication resolution is identical when using ``HASURA_GRAPHQL_JWT_SECRET`` or a single ``HASURA_GRAPHQL_JWT_SECRETS`` configuration.
|
||||
|
||||
.. note::
|
||||
|
||||
If both ``HASURA_GRAPHQL_JWT_SECRET`` and ``HASURA_GRAPHQL_JWT_SECRETS`` are set, then ``HASURA_GRAPHQL_JWT_SECRETS`` will be used.
|
@ -378,6 +378,16 @@ Examples:
|
||||
Certain providers require you to verify the ``iss`` claim on the JWT. To do
|
||||
that you can set this field to the appropriate value.
|
||||
|
||||
.. note::
|
||||
|
||||
A JWT configuration without an issuer will match any issuer field present in
|
||||
an incoming JWT.
|
||||
|
||||
.. note::
|
||||
|
||||
An incoming JWT without an issuer specified will match a configuration
|
||||
even if it specifies an issuer.
|
||||
|
||||
|
||||
``claims_map``
|
||||
^^^^^^^^^^^^^^
|
||||
|
@ -157,6 +157,7 @@ library
|
||||
, mtl
|
||||
, openapi3
|
||||
, optparse-applicative
|
||||
, optparse-generic
|
||||
, parsec
|
||||
, pg-client
|
||||
, postgresql-binary
|
||||
|
@ -21,6 +21,7 @@ module Hasura.Prelude
|
||||
liftEitherM,
|
||||
hoistMaybe,
|
||||
hoistEither,
|
||||
readJson,
|
||||
tshow,
|
||||
|
||||
-- * Trace debugging
|
||||
@ -267,6 +268,9 @@ hoistEither = ExceptT . pure
|
||||
tshow :: Show a => a -> Text
|
||||
tshow = T.pack . show
|
||||
|
||||
readJson :: (J.FromJSON a) => String -> Either String a
|
||||
readJson = J.eitherDecodeStrict . txtToBs . T.pack
|
||||
|
||||
-- | Customized 'J.Options' which apply "snake case" to Generic or Template
|
||||
-- Haskell JSON derivations.
|
||||
--
|
||||
|
@ -35,7 +35,7 @@ data ServerConfig = ServerConfig
|
||||
scfgIsAdminSecretSet :: !Bool,
|
||||
scfgIsAuthHookSet :: !Bool,
|
||||
scfgIsJwtSet :: !Bool,
|
||||
scfgJwt :: !(Maybe JWTInfo),
|
||||
scfgJwt :: ![JWTInfo],
|
||||
scfgIsAllowListEnabled :: !Bool,
|
||||
scfgLiveQueries :: !LQ.LiveQueriesOptions,
|
||||
scfgConsoleAssetsDir :: !(Maybe Text),
|
||||
@ -90,11 +90,12 @@ isJWTSet = \case
|
||||
AMAdminSecretAndJWT {} -> True
|
||||
_ -> False
|
||||
|
||||
getJWTInfo :: AuthMode -> Maybe JWTInfo
|
||||
getJWTInfo (AMAdminSecretAndJWT _ jwtCtx _) =
|
||||
Just $ case jcxClaims jwtCtx of
|
||||
JCNamespace namespace claimsFormat ->
|
||||
JWTInfo namespace claimsFormat Nothing
|
||||
JCMap claimsMap ->
|
||||
JWTInfo (ClaimNs defaultClaimsNamespace) defaultClaimsFormat $ Just claimsMap
|
||||
getJWTInfo _ = Nothing
|
||||
getJWTInfo :: AuthMode -> [JWTInfo]
|
||||
getJWTInfo (AMAdminSecretAndJWT _ jwtCtxs _) =
|
||||
let f jwtCtx = case jcxClaims jwtCtx of
|
||||
JCNamespace namespace claimsFormat ->
|
||||
JWTInfo namespace claimsFormat Nothing
|
||||
JCMap claimsMap ->
|
||||
JWTInfo (ClaimNs defaultClaimsNamespace) defaultClaimsFormat $ Just claimsMap
|
||||
in fmap f jwtCtxs
|
||||
getJWTInfo _ = mempty
|
||||
|
@ -36,6 +36,7 @@ import Data.ByteString (ByteString)
|
||||
import Data.HashSet qualified as Set
|
||||
import Data.Hashable qualified as Hash
|
||||
import Data.IORef (newIORef)
|
||||
import Data.List qualified as L
|
||||
import Data.Text.Encoding qualified as T
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Hasura.Base.Error
|
||||
@ -96,7 +97,7 @@ data AuthMode
|
||||
= AMNoAuth
|
||||
| AMAdminSecret !(Set.HashSet AdminSecretHash) !(Maybe RoleName)
|
||||
| AMAdminSecretAndHook !(Set.HashSet AdminSecretHash) !AuthHook
|
||||
| AMAdminSecretAndJWT !(Set.HashSet AdminSecretHash) !JWTCtx !(Maybe RoleName)
|
||||
| AMAdminSecretAndJWT !(Set.HashSet AdminSecretHash) ![JWTCtx] !(Maybe RoleName)
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Validate the user's requested authentication configuration, launching any
|
||||
@ -109,17 +110,17 @@ setupAuthMode ::
|
||||
) =>
|
||||
Set.HashSet AdminSecretHash ->
|
||||
Maybe AuthHook ->
|
||||
Maybe JWTConfig ->
|
||||
[JWTConfig] ->
|
||||
Maybe RoleName ->
|
||||
H.Manager ->
|
||||
Logger Hasura ->
|
||||
ExceptT Text (ManagedT m) AuthMode
|
||||
setupAuthMode adminSecretHashSet mWebHook mJwtSecret mUnAuthRole httpManager logger =
|
||||
case (not (Set.null adminSecretHashSet), mWebHook, mJwtSecret) of
|
||||
(True, Nothing, Nothing) -> return $ AMAdminSecret adminSecretHashSet mUnAuthRole
|
||||
(True, Nothing, Just jwtConf) -> do
|
||||
jwtCtx <- mkJwtCtx jwtConf
|
||||
return $ AMAdminSecretAndJWT adminSecretHashSet jwtCtx mUnAuthRole
|
||||
setupAuthMode adminSecretHashSet mWebHook mJwtSecrets mUnAuthRole httpManager logger =
|
||||
case (not (Set.null adminSecretHashSet), mWebHook, not (null mJwtSecrets)) of
|
||||
(True, Nothing, False) -> return $ AMAdminSecret adminSecretHashSet mUnAuthRole
|
||||
(True, Nothing, True) -> do
|
||||
jwtCtxs <- traverse mkJwtCtx (L.nub mJwtSecrets)
|
||||
pure $ AMAdminSecretAndJWT adminSecretHashSet jwtCtxs mUnAuthRole
|
||||
-- Nothing below this case uses unauth role. Throw a fatal error if we would otherwise ignore
|
||||
-- that parameter, lest users misunderstand their auth configuration:
|
||||
_
|
||||
@ -128,15 +129,15 @@ setupAuthMode adminSecretHashSet mWebHook mJwtSecret mUnAuthRole httpManager log
|
||||
"Fatal Error: --unauthorized-role (HASURA_GRAPHQL_UNAUTHORIZED_ROLE)"
|
||||
<> requiresAdminScrtMsg
|
||||
<> " and is not allowed when --auth-hook (HASURA_GRAPHQL_AUTH_HOOK) is set"
|
||||
(False, Nothing, Nothing) -> return AMNoAuth
|
||||
(True, Just hook, Nothing) -> return $ AMAdminSecretAndHook adminSecretHashSet hook
|
||||
(False, Just _, Nothing) ->
|
||||
(False, Nothing, False) -> return AMNoAuth
|
||||
(True, Just hook, False) -> return $ AMAdminSecretAndHook adminSecretHashSet hook
|
||||
(False, Just _, False) ->
|
||||
throwError $
|
||||
"Fatal Error : --auth-hook (HASURA_GRAPHQL_AUTH_HOOK)" <> requiresAdminScrtMsg
|
||||
(False, Nothing, Just _) ->
|
||||
(False, Nothing, True) ->
|
||||
throwError $
|
||||
"Fatal Error : --jwt-secret (HASURA_GRAPHQL_JWT_SECRET)" <> requiresAdminScrtMsg
|
||||
(_, Just _, Just _) ->
|
||||
(_, Just _, True) ->
|
||||
throwError
|
||||
"Fatal Error: Both webhook and JWT mode cannot be enabled at the same time"
|
||||
where
|
||||
@ -204,7 +205,7 @@ getUserInfoWithExpTime_ ::
|
||||
m (UserInfo, Maybe UTCTime, [N.Header])
|
||||
) ->
|
||||
-- | mock 'processJwt'
|
||||
(JWTCtx -> [N.Header] -> Maybe RoleName -> m (UserInfo, Maybe UTCTime, [N.Header])) ->
|
||||
([JWTCtx] -> [N.Header] -> Maybe RoleName -> m (UserInfo, Maybe UTCTime, [N.Header])) ->
|
||||
_Logger_Hasura ->
|
||||
_Manager ->
|
||||
[N.Header] ->
|
||||
@ -232,8 +233,8 @@ getUserInfoWithExpTime_ userInfoFromAuthHook_ processJwt_ logger manager rawHead
|
||||
-- this is the case that actually ends up consuming the request AST
|
||||
AMAdminSecretAndHook adminSecretHashSet hook ->
|
||||
checkingSecretIfSent adminSecretHashSet $ userInfoFromAuthHook_ logger manager hook rawHeaders reqs
|
||||
AMAdminSecretAndJWT adminSecretHashSet jwtSecret unAuthRole ->
|
||||
checkingSecretIfSent adminSecretHashSet $ processJwt_ jwtSecret rawHeaders unAuthRole
|
||||
AMAdminSecretAndJWT adminSecretHashSet jwtSecrets unAuthRole ->
|
||||
checkingSecretIfSent adminSecretHashSet $ processJwt_ jwtSecrets rawHeaders unAuthRole
|
||||
where
|
||||
-- CAREFUL!:
|
||||
mkUserInfoFallbackAdminRole adminSecretState =
|
||||
|
@ -1,6 +1,28 @@
|
||||
-- |
|
||||
-- Module : Hasura.Server.Auth.JWT
|
||||
-- Description : Implements JWT Configuration and Validation Logic.
|
||||
-- Copyright : Hasura
|
||||
--
|
||||
-- This module implements the bulk of Hasura's JWT capabilities and interactions.
|
||||
-- Its main point of non-testing invocation is `Hasura.Server.Auth`.
|
||||
--
|
||||
-- It exports both `processJwt` and `processJwt_` with `processJwt_` being the
|
||||
-- majority of the implementation with the JWT Token processing function
|
||||
-- passed in as an argument in order to enable mocking in test-code.
|
||||
--
|
||||
-- In `processJwt_`, prior to validation of the token, first the token locations
|
||||
-- and issuers are reconciled. Locations are either specified as auth or
|
||||
-- cookie (with cookie name) or assumed to be auth. Issuers can be omitted or
|
||||
-- specified, where an omitted configured issuer can match any issuer specified by
|
||||
-- a request.
|
||||
--
|
||||
-- If none match, then this is considered an no-auth request, if one matches,
|
||||
-- then normal token auth is performed, and if multiple match, then this is
|
||||
-- considered an ambiguity error.
|
||||
module Hasura.Server.Auth.JWT
|
||||
( processJwt,
|
||||
RawJWT,
|
||||
StringOrURI (..),
|
||||
JWTConfig (..),
|
||||
JWTCtx (..),
|
||||
Jose.JWKSet (..),
|
||||
@ -20,6 +42,7 @@ module Hasura.Server.Auth.JWT
|
||||
|
||||
-- * Exposed for testing
|
||||
processJwt_,
|
||||
tokenIssuer,
|
||||
allowedRolesClaim,
|
||||
defaultRoleClaim,
|
||||
parseClaimsMap,
|
||||
@ -38,10 +61,14 @@ import Data.Aeson qualified as J
|
||||
import Data.Aeson.Casing qualified as J
|
||||
import Data.Aeson.Internal (JSONPath)
|
||||
import Data.Aeson.TH qualified as J
|
||||
import Data.ByteArray.Encoding qualified as BAE
|
||||
import Data.ByteString.Char8 qualified as BC
|
||||
import Data.ByteString.Internal qualified as B
|
||||
import Data.ByteString.Lazy qualified as BL
|
||||
import Data.ByteString.Lazy.Char8 qualified as BLC
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.Hashable
|
||||
import Data.IORef (IORef, readIORef, writeIORef)
|
||||
import Data.Parser.CacheControl
|
||||
import Data.Parser.Expires
|
||||
@ -93,7 +120,9 @@ $( J.deriveJSON
|
||||
data JWTHeader
|
||||
= JHAuthorization
|
||||
| JHCookie Text -- cookie name
|
||||
deriving (Show, Eq)
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance Hashable JWTHeader
|
||||
|
||||
instance J.FromJSON JWTHeader where
|
||||
parseJSON = J.withObject "JWTHeader" $ \o -> do
|
||||
@ -217,6 +246,21 @@ data JWTClaims
|
||||
| JCMap !JWTCustomClaimsMap
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Hashable Wrapper for constructing a HashMap of JWTConfigs
|
||||
newtype StringOrURI = StringOrURI {unStringOrURI :: Jose.StringOrURI}
|
||||
deriving newtype (Show, Eq, J.ToJSON, J.FromJSON)
|
||||
|
||||
instance J.ToJSONKey StringOrURI
|
||||
|
||||
instance J.FromJSONKey StringOrURI
|
||||
|
||||
instance J.ToJSONKey (Maybe StringOrURI)
|
||||
|
||||
instance J.FromJSONKey (Maybe StringOrURI)
|
||||
|
||||
instance Hashable StringOrURI where
|
||||
hashWithSalt i = hashWithSalt i . J.encode
|
||||
|
||||
-- | The JWT configuration we got from the user.
|
||||
data JWTConfig = JWTConfig
|
||||
{ jcKeyOrUrl :: !(Either Jose.JWK URI),
|
||||
@ -377,6 +421,21 @@ determineJwkExpiryLifetime getCurrentTime' (Logger logger) responseHeaders =
|
||||
|
||||
type ClaimsMap = Map.HashMap SessionVariable J.Value
|
||||
|
||||
-- | Decode a Jose ClaimsSet without verifying the signature
|
||||
decodeClaimsSet :: RawJWT -> Maybe Jose.ClaimsSet
|
||||
decodeClaimsSet (RawJWT jwt) = do
|
||||
(_, c, _) <- extractElems $ BL.splitWith (== B.c2w '.') jwt
|
||||
case BAE.convertFromBase BAE.Base64URLUnpadded $ BL.toStrict c of
|
||||
Left _ -> Nothing
|
||||
Right s -> J.decode $ BL.fromStrict s
|
||||
where
|
||||
extractElems (h : c : s : _) = Just (h, c, s)
|
||||
extractElems _ = Nothing
|
||||
|
||||
-- | Extract the issuer from a bearer tokena _without_ verifying it.
|
||||
tokenIssuer :: RawJWT -> Maybe StringOrURI
|
||||
tokenIssuer = coerce <$> (decodeClaimsSet >=> view Jose.claimIss)
|
||||
|
||||
-- | Process the request headers to verify the JWT and extract UserInfo from it
|
||||
-- From the JWT config, we check which header to expect, it can be the "Authorization"
|
||||
-- or "Cookie" header
|
||||
@ -393,69 +452,115 @@ processJwt ::
|
||||
( MonadIO m,
|
||||
MonadError QErr m
|
||||
) =>
|
||||
JWTCtx ->
|
||||
[JWTCtx] ->
|
||||
HTTP.RequestHeaders ->
|
||||
Maybe RoleName ->
|
||||
m (UserInfo, Maybe UTCTime, [N.Header])
|
||||
processJwt = processJwt_ processAuthZOrCookieHeader jcxHeader
|
||||
processJwt = processJwt_ processHeaderSimple tokenIssuer jcxHeader
|
||||
|
||||
type AuthTokenLocation = JWTHeader
|
||||
|
||||
-- Broken out for testing with mocks:
|
||||
processJwt_ ::
|
||||
(MonadError QErr m) =>
|
||||
-- | mock 'processAuthZOrCookieHeader'
|
||||
(_JWTCtx -> BLC.ByteString -> m (Maybe (ClaimsMap, Maybe UTCTime))) ->
|
||||
(_JWTCtx -> JWTHeader) ->
|
||||
_JWTCtx ->
|
||||
(JWTCtx -> BLC.ByteString -> m (ClaimsMap, Maybe UTCTime)) ->
|
||||
(RawJWT -> Maybe StringOrURI) ->
|
||||
(JWTCtx -> JWTHeader) ->
|
||||
[JWTCtx] ->
|
||||
HTTP.RequestHeaders ->
|
||||
Maybe RoleName ->
|
||||
m (UserInfo, Maybe UTCTime, [N.Header])
|
||||
processJwt_ processAuthZOrCookieHeader_ fGetHeaderType jwtCtx headers mUnAuthRole =
|
||||
maybe withoutAuthZHeader withAuthZHeader mAuthZHeader
|
||||
processJwt_ processJwtBytes decodeIssuer fGetHeaderType jwtCtxs headers mUnAuthRole = do
|
||||
-- Here we use `intersectKeys` to match up the correct locations of JWTs to those specified in JWTCtxs
|
||||
-- Then we match up issuers, where no-issuer specified in a JWTCtx can match any issuer in a JWT
|
||||
-- Then there should either be zero matches - Perform no auth
|
||||
-- Or one match - Perform normal auth
|
||||
-- Otherwise there is an ambiguous situation which we currently treat as an error.
|
||||
issuerMatches <- traverse issuerMatch $ intersectKeys (keyCtxOnAuthTypes jwtCtxs) (keyTokensOnAuthTypes headers)
|
||||
|
||||
-- ltraceM "issuerMatches" issuerMatches
|
||||
|
||||
case (lefts issuerMatches, rights issuerMatches) of
|
||||
([], []) -> withoutAuthZ
|
||||
(_ : _, []) -> jwtNotIssuerError
|
||||
(_, [(ctx, val)]) -> withAuthZ val ctx
|
||||
_ -> throw400 InvalidHeaders "Could not verify JWT: Multiple JWTs found"
|
||||
where
|
||||
expectedHeader =
|
||||
intersectKeys :: (Hashable a, Eq a) => Map.HashMap a [b] -> Map.HashMap a [c] -> [(b, c)]
|
||||
intersectKeys m n = concatMap (uncurry cartesianProduct) $ Map.elems $ Map.intersectionWith (,) m n
|
||||
|
||||
issuerMatch (j, b) = do
|
||||
b'' <- case b of
|
||||
(JHCookie _, b') -> pure b'
|
||||
(JHAuthorization, b') ->
|
||||
case BC.words b' of
|
||||
["Bearer", jwt] -> pure jwt
|
||||
_ -> throw400 InvalidHeaders "Malformed Authorization header"
|
||||
|
||||
case (StringOrURI <$> jcxIssuer j, decodeIssuer $ RawJWT $ BLC.fromStrict b'') of
|
||||
(Nothing, _) -> pure $ Right (j, b'')
|
||||
(_, Nothing) -> pure $ Right (j, b'')
|
||||
(ci, ji)
|
||||
| ci == ji -> pure $ Right (j, b'')
|
||||
| otherwise -> pure $ Left (ci, ji, j, b'')
|
||||
|
||||
cartesianProduct :: [a] -> [b] -> [(a, b)]
|
||||
cartesianProduct as bs = [(a, b) | a <- as, b <- bs]
|
||||
|
||||
keyCtxOnAuthTypes :: [JWTCtx] -> Map.HashMap AuthTokenLocation [JWTCtx]
|
||||
keyCtxOnAuthTypes = Map.fromListWith (++) . fmap (expectedHeader &&& pure)
|
||||
|
||||
keyTokensOnAuthTypes :: [HTTP.Header] -> Map.HashMap AuthTokenLocation [(AuthTokenLocation, B.ByteString)]
|
||||
keyTokensOnAuthTypes = Map.fromListWith (++) . map (fst &&& pure) . concatMap findTokensInHeader
|
||||
|
||||
findTokensInHeader :: Header -> [(AuthTokenLocation, B.ByteString)]
|
||||
findTokensInHeader (key, val)
|
||||
| key == CI.mk "Authorization" = [(JHAuthorization, val)]
|
||||
| key == CI.mk "Cookie" = bimap JHCookie T.encodeUtf8 <$> Spock.parseCookies val
|
||||
| otherwise = []
|
||||
|
||||
expectedHeader :: JWTCtx -> AuthTokenLocation
|
||||
expectedHeader jwtCtx =
|
||||
case fGetHeaderType jwtCtx of
|
||||
JHAuthorization -> "Authorization"
|
||||
JHCookie _ -> "Cookie"
|
||||
JHAuthorization -> JHAuthorization
|
||||
JHCookie name -> JHCookie name
|
||||
|
||||
mAuthZHeader =
|
||||
find (\(headerName, _) -> headerName == CI.mk expectedHeader) headers
|
||||
withAuthZ authzHeader jwtCtx = do
|
||||
authMode <- processJwtBytes jwtCtx $ BL.fromStrict authzHeader
|
||||
|
||||
withAuthZHeader (_, authzHeader) = do
|
||||
claimsMapTuple <- processAuthZOrCookieHeader_ jwtCtx (BL.fromStrict authzHeader)
|
||||
let (claimsMap, expTimeM) = authMode
|
||||
in do
|
||||
HasuraClaims allowedRoles defaultRole <- parseHasuraClaims claimsMap
|
||||
-- see if there is a x-hasura-role header, or else pick the default role.
|
||||
-- The role returned is unauthenticated at this point:
|
||||
let requestedRole =
|
||||
fromMaybe defaultRole $
|
||||
getRequestHeader userRoleHeader headers >>= mkRoleName . bsToTxt
|
||||
|
||||
case claimsMapTuple of
|
||||
Nothing -> withoutAuthZHeader
|
||||
Just (claimsMap, expTimeM) -> do
|
||||
HasuraClaims allowedRoles defaultRole <- parseHasuraClaims claimsMap
|
||||
let requestedRole = fromMaybe defaultRole $ getRequestHeader userRoleHeader headers >>= mkRoleName . bsToTxt
|
||||
when (requestedRole `notElem` allowedRoles) $
|
||||
throw400 AccessDenied "Your requested role is not in allowed roles"
|
||||
let finalClaims =
|
||||
Map.delete defaultRoleClaim . Map.delete allowedRolesClaim $ claimsMap
|
||||
|
||||
when (requestedRole `notElem` allowedRoles) $
|
||||
throw400 AccessDenied "Your requested role is not in allowed roles"
|
||||
let finalClaims =
|
||||
Map.delete defaultRoleClaim . Map.delete allowedRolesClaim $ claimsMap
|
||||
let finalClaimsObject = mapKeys sessionVariableToText finalClaims
|
||||
metadata <- parseJwtClaim (J.Object finalClaimsObject) "x-hasura-* claims"
|
||||
userInfo <-
|
||||
mkUserInfo (URBPreDetermined requestedRole) UAdminSecretNotSent $
|
||||
mkSessionVariablesText metadata
|
||||
pure (userInfo, expTimeM, [])
|
||||
|
||||
let finalClaimsObject = mapKeys sessionVariableToText finalClaims
|
||||
metadata <- parseJwtClaim (J.Object $ finalClaimsObject) "x-hasura-* claims"
|
||||
userInfo <-
|
||||
mkUserInfo (URBPreDetermined requestedRole) UAdminSecretNotSent $
|
||||
mkSessionVariablesText metadata
|
||||
pure (userInfo, expTimeM, [])
|
||||
|
||||
withoutAuthZHeader = do
|
||||
unAuthRole <- onNothing mUnAuthRole missingAuthzHeader
|
||||
withoutAuthZ = do
|
||||
unAuthRole <- onNothing mUnAuthRole (throw400 InvalidHeaders "Missing 'Authorization' or 'Cookie' header in JWT authentication mode")
|
||||
userInfo <-
|
||||
mkUserInfo (URBPreDetermined unAuthRole) UAdminSecretNotSent $
|
||||
mkSessionVariablesHeaders headers
|
||||
pure (userInfo, Nothing, [])
|
||||
where
|
||||
missingAuthzHeader =
|
||||
throw400 InvalidHeaders $
|
||||
"Missing " <> bsToTxt expectedHeader <> " header in JWT authentication mode"
|
||||
|
||||
-- | Parse and verify the 'Authorization' or 'Cookie' header (depending upon
|
||||
-- the `jcHeader` value of the `JWTConfig`), returning either Nothing or the raw claims
|
||||
-- object, and the expiration, if any.
|
||||
processAuthZOrCookieHeader ::
|
||||
jwtNotIssuerError = throw400 JWTInvalid "Could not verify JWT: JWTNotInIssuer"
|
||||
|
||||
-- | Processes a token payload (excluding the `Bearer ` prefix in the context of a JWTCtx)
|
||||
processHeaderSimple ::
|
||||
( MonadIO m,
|
||||
MonadError QErr m
|
||||
) =>
|
||||
@ -463,48 +568,29 @@ processAuthZOrCookieHeader ::
|
||||
BLC.ByteString ->
|
||||
-- The "Maybe" in "m (Maybe (...))" covers the case where the
|
||||
-- requested Cookie name is not present (returns "m Nothing")
|
||||
m (Maybe (ClaimsMap, Maybe UTCTime))
|
||||
processAuthZOrCookieHeader jwtCtx authzHeader = do
|
||||
m (ClaimsMap, Maybe UTCTime)
|
||||
processHeaderSimple jwtCtx jwt = do
|
||||
--iss <- _ <$> Jose.decodeCompact (BL.fromStrict token)
|
||||
--let ctx = M.lookup iss jwtCtx
|
||||
|
||||
-- try to parse JWT token from Authorization or Cookie header
|
||||
jwt <-
|
||||
case jcxHeader jwtCtx of
|
||||
JHAuthorization -> Just <$> parseAuthzHeader
|
||||
JHCookie cName -> parseCookieHeader cName
|
||||
|
||||
-- verify the JWT
|
||||
case jwt of
|
||||
Nothing -> pure Nothing
|
||||
Just jwt' -> do
|
||||
claims <- liftJWTError invalidJWTError $ verifyJwt jwtCtx $ RawJWT jwt'
|
||||
claims <- liftJWTError invalidJWTError $ verifyJwt jwtCtx $ RawJWT jwt
|
||||
|
||||
let expTimeM = fmap (\(Jose.NumericDate t) -> t) $ claims ^. Jose.claimExp
|
||||
let expTimeM = fmap (\(Jose.NumericDate t) -> t) $ claims ^. Jose.claimExp
|
||||
|
||||
claimsObject <- parseClaimsMap claims claimsConfig
|
||||
claimsObject <- parseClaimsMap claims claimsConfig
|
||||
|
||||
pure $ Just (claimsObject, expTimeM)
|
||||
pure (claimsObject, expTimeM)
|
||||
where
|
||||
claimsConfig = jcxClaims jwtCtx
|
||||
parseAuthzHeader = do
|
||||
let tokenParts = BLC.words authzHeader
|
||||
case tokenParts of
|
||||
["Bearer", jwt] -> return jwt
|
||||
_ -> malformedAuthzHeader
|
||||
|
||||
parseCookieHeader cName = do
|
||||
let cookies = Spock.parseCookies $ BL.toStrict authzHeader
|
||||
jwtCookie = snd <$> find (\(hn, _) -> hn == cName) cookies
|
||||
pure $ BL.fromStrict . txtToBs <$> jwtCookie
|
||||
|
||||
liftJWTError :: (MonadError e' m) => (e -> e') -> ExceptT e m a -> m a
|
||||
liftJWTError ef action = do
|
||||
res <- runExceptT action
|
||||
onLeft res (throwError . ef)
|
||||
|
||||
invalidJWTError e =
|
||||
err400 JWTInvalid $ "Could not verify JWT: " <> tshow e
|
||||
|
||||
malformedAuthzHeader =
|
||||
throw400 InvalidHeaders "Malformed Authorization header"
|
||||
invalidJWTError e = err400 JWTInvalid $ "Could not verify JWT: " <> tshow e
|
||||
|
||||
-- | parse the claims map from the JWT token or custom claims from the JWT config
|
||||
parseClaimsMap ::
|
||||
|
@ -187,7 +187,8 @@ mkServeOptions rso = do
|
||||
txIso <- fromMaybe Q.ReadCommitted <$> withEnv (rsoTxIso rso) (fst txIsoEnv)
|
||||
adminScrt <- fmap (maybe mempty Set.singleton) $ withEnvs (rsoAdminSecret rso) $ map fst [adminSecretEnv, accessKeyEnv]
|
||||
authHook <- mkAuthHook $ rsoAuthHook rso
|
||||
jwtSecret <- withEnvJwtConf (rsoJwtSecret rso) $ fst jwtSecretEnv
|
||||
jwtSecret <- (`onNothing` mempty) <$> withEnvJwtConf (rsoJwtSecret rso) (fst jwtSecretEnv)
|
||||
|
||||
unAuthRole <- withEnv (rsoUnAuthRole rso) $ fst unAuthRoleEnv
|
||||
corsCfg <- mkCorsConfig $ rsoCorsConfig rso
|
||||
enableConsole <-
|
||||
|
@ -220,7 +220,7 @@ data ServeOptions impl = ServeOptions
|
||||
soTxIso :: Q.TxIsolation,
|
||||
soAdminSecret :: Set.HashSet AdminSecretHash,
|
||||
soAuthHook :: Maybe AuthHook,
|
||||
soJwtSecret :: Maybe JWTConfig,
|
||||
soJwtSecret :: [JWTConfig],
|
||||
soUnAuthRole :: Maybe RoleName,
|
||||
soCorsConfig :: CorsConfig,
|
||||
soEnableConsole :: Bool,
|
||||
@ -378,9 +378,6 @@ readLogLevel s = case T.toLower $ T.strip $ T.pack s of
|
||||
"error" -> Right L.LevelError
|
||||
_ -> Left "Valid log levels: debug, info, warn or error"
|
||||
|
||||
readJson :: (J.FromJSON a) => String -> Either String a
|
||||
readJson = J.eitherDecodeStrict . txtToBs . T.pack
|
||||
|
||||
class FromEnv a where
|
||||
fromEnv :: String -> Either String a
|
||||
|
||||
@ -447,6 +444,9 @@ instance FromEnv Seconds where
|
||||
instance FromEnv JWTConfig where
|
||||
fromEnv = readJson
|
||||
|
||||
instance FromEnv [JWTConfig] where
|
||||
fromEnv = readJson
|
||||
|
||||
instance L.EnabledLogTypes impl => FromEnv [L.EngineLogType impl] where
|
||||
fromEnv = L.parseEnabledLogTypes
|
||||
|
||||
|
@ -67,9 +67,9 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
(mkSessionVariablesHeaders mempty)
|
||||
|
||||
processAuthZHeader _jwtCtx _authzHeader =
|
||||
return $ Just (mapKeys mkSessionVariable claims, Nothing)
|
||||
pure (mapKeys mkSessionVariable claims, Nothing)
|
||||
|
||||
processJwt = processJwt_ processAuthZHeader (const JHAuthorization)
|
||||
processJwt = processJwt_ processAuthZHeader tokenIssuer (const JHAuthorization)
|
||||
|
||||
let getUserInfoWithExpTime ::
|
||||
J.Object ->
|
||||
@ -85,17 +85,17 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
|
||||
describe "started without admin secret" $ do
|
||||
it "gives admin by default" $ do
|
||||
mode <- setupAuthMode'E Nothing Nothing Nothing Nothing
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
mode <- setupAuthMode'E Nothing Nothing mempty Nothing
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
`shouldReturn` Right adminRoleName
|
||||
it "allows any requested role" $ do
|
||||
mode <- setupAuthMode'E Nothing Nothing Nothing Nothing
|
||||
mode <- setupAuthMode'E Nothing Nothing mempty Nothing
|
||||
getUserInfoWithExpTime mempty [(userRoleHeader, "r00t")] mode
|
||||
`shouldReturn` Right (mkRoleNameE "r00t")
|
||||
|
||||
describe "admin secret only" $ do
|
||||
describe "unauth role NOT set" $ do
|
||||
mode <- runIO $ setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing Nothing Nothing
|
||||
mode <- runIO $ setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing mempty Nothing
|
||||
|
||||
it "accepts when admin secret matches" $ do
|
||||
getUserInfoWithExpTime mempty [(adminSecretHeader, "secret")] mode
|
||||
@ -120,7 +120,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
`shouldReturn` Left AccessDenied
|
||||
|
||||
it "rejects when no secret sent, since no fallback unauth role" $ do
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
getUserInfoWithExpTime mempty [(userRoleHeader, "r00t"), (userRoleHeader, "admin")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
@ -132,7 +132,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
describe "unauth role set" $ do
|
||||
mode <-
|
||||
runIO $
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing Nothing (Just ourUnauthRole)
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing mempty (Just ourUnauthRole)
|
||||
it "accepts when admin secret matches" $ do
|
||||
getUserInfoWithExpTime mempty [(adminSecretHeader, "secret")] mode
|
||||
`shouldReturn` Right adminRoleName
|
||||
@ -158,7 +158,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
`shouldReturn` Left AccessDenied
|
||||
|
||||
it "accepts when no secret sent and unauth role defined" $ do
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
`shouldReturn` Right ourUnauthRole
|
||||
getUserInfoWithExpTime mempty [("heh", "heh")] mode
|
||||
`shouldReturn` Right ourUnauthRole
|
||||
@ -170,7 +170,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
describe "webhook" $ do
|
||||
mode <-
|
||||
runIO $
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") (Just fakeAuthHook) Nothing Nothing
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") (Just fakeAuthHook) mempty Nothing
|
||||
|
||||
it "accepts when admin secret matches" $ do
|
||||
getUserInfoWithExpTime mempty [(adminSecretHeader, "secret")] mode
|
||||
@ -195,7 +195,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
`shouldReturn` Left AccessDenied
|
||||
|
||||
it "authenticates with webhook when no admin secret sent" $ do
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
`shouldReturn` Right (mkRoleNameE "hook")
|
||||
getUserInfoWithExpTime mempty [("blah", "blah")] mode
|
||||
`shouldReturn` Right (mkRoleNameE "hook")
|
||||
@ -218,7 +218,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
describe "unauth role NOT set" $ do
|
||||
mode <-
|
||||
runIO $
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing (Just fakeJWTConfig) Nothing
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing [fakeJWTConfig] Nothing
|
||||
|
||||
it "accepts when admin secret matches" $ do
|
||||
getUserInfoWithExpTime mempty [(adminSecretHeader, "secret")] mode
|
||||
@ -245,7 +245,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
it "rejects when admin secret not sent and no 'Authorization' header" $ do
|
||||
getUserInfoWithExpTime mempty [("blah", "blah")] mode
|
||||
`shouldReturn` Left InvalidHeaders
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
`shouldReturn` Left InvalidHeaders
|
||||
|
||||
describe "unauth role set" $ do
|
||||
@ -254,7 +254,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
setupAuthMode'E
|
||||
(Just $ Set.singleton $ hashAdminSecret "secret")
|
||||
Nothing
|
||||
(Just fakeJWTConfig)
|
||||
[fakeJWTConfig]
|
||||
(Just ourUnauthRole)
|
||||
|
||||
it "accepts when admin secret matches" $ do
|
||||
@ -284,7 +284,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
it "authorizes as unauth role when no 'Authorization' header" $ do
|
||||
getUserInfoWithExpTime mempty [("blah", "blah")] mode
|
||||
`shouldReturn` Right ourUnauthRole
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
`shouldReturn` Right ourUnauthRole
|
||||
|
||||
describe "when Authorization header sent, and no admin secret" $ do
|
||||
@ -293,14 +293,14 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
setupAuthMode'E
|
||||
(Just $ Set.singleton $ hashAdminSecret "secret")
|
||||
Nothing
|
||||
(Just fakeJWTConfig)
|
||||
[fakeJWTConfig]
|
||||
(Just ourUnauthRole)
|
||||
modeB <-
|
||||
runIO $
|
||||
setupAuthMode'E
|
||||
(Just $ Set.singleton $ hashAdminSecret "secret")
|
||||
Nothing
|
||||
(Just fakeJWTConfig)
|
||||
[fakeJWTConfig]
|
||||
Nothing
|
||||
|
||||
-- Here the unauth role does not come into play at all, so map same tests over both modes:
|
||||
@ -312,10 +312,10 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
[ allowedRolesClaimText .= (["editor", "user", "mod"] :: [Text]),
|
||||
defaultRoleClaimText .= ("user" :: Text)
|
||||
]
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "editor")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED"), (userRoleHeader, "editor")] mode
|
||||
`shouldReturn` Right (mkRoleNameE "editor")
|
||||
-- Uses the defaultRoleClaimText:
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED")] mode
|
||||
`shouldReturn` Right (mkRoleNameE "user")
|
||||
|
||||
it "rejects when requested role is not allowed" $ do
|
||||
@ -324,27 +324,27 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
[ allowedRolesClaimText .= (["editor", "user", "mod"] :: [Text]),
|
||||
defaultRoleClaimText .= ("user" :: Text)
|
||||
]
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "r00t")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED"), (userRoleHeader, "r00t")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "admin")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED"), (userRoleHeader, "admin")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
|
||||
-- A corner case, but the behavior seems desirable:
|
||||
it "always rejects when token has empty allowedRolesClaimText" $ do
|
||||
let claim = unObject [allowedRolesClaimText .= ([] :: [Text]), defaultRoleClaimText .= ("user" :: Text)]
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "admin")] mode
|
||||
let claim = unObject [allowedRolesClaimText .= (mempty :: [Text]), defaultRoleClaimText .= ("user" :: Text)]
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED"), (userRoleHeader, "admin")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "user")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED"), (userRoleHeader, "user")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
|
||||
it "rejects when token doesn't have proper allowedRolesClaimText and defaultRoleClaimText" $ do
|
||||
let claim0 = unObject [allowedRolesClaimText .= (["editor", "user", "mod"] :: [Text])]
|
||||
claim1 = unObject [defaultRoleClaimText .= ("user" :: Text)]
|
||||
claim2 = unObject []
|
||||
claim2 = unObject mempty
|
||||
for_ [claim0, claim1, claim2] $ \claim ->
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED")] mode
|
||||
getUserInfoWithExpTime claim [("Authorization", "Bearer IGNORED")] mode
|
||||
`shouldReturn` Left JWTRoleClaimMissing
|
||||
|
||||
-- (*) FIXME NOTE (re above):
|
||||
@ -370,53 +370,53 @@ setupAuthModeTests = describe "setupAuthMode" $ do
|
||||
|
||||
-- These are all various error cases, except for the AMNoAuth mode:
|
||||
it "with no admin secret provided" $ do
|
||||
setupAuthMode' Nothing Nothing Nothing Nothing
|
||||
setupAuthMode' Nothing Nothing mempty Nothing
|
||||
`shouldReturn` Right AMNoAuth
|
||||
-- We insist on an admin secret in order to use webhook or JWT auth:
|
||||
setupAuthMode' Nothing Nothing (Just fakeJWTConfig) Nothing
|
||||
setupAuthMode' Nothing Nothing [fakeJWTConfig] Nothing
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) Nothing Nothing
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) mempty Nothing
|
||||
`shouldReturn` Left ()
|
||||
-- ...and we can't have both:
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) (Just fakeJWTConfig) Nothing
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) [fakeJWTConfig] Nothing
|
||||
`shouldReturn` Left ()
|
||||
-- If the unauthenticated role was set but would otherwise be ignored this
|
||||
-- should be an error (for now), since users might expect all access to use
|
||||
-- the specified role. This first case would be the real worrying one:
|
||||
setupAuthMode' Nothing Nothing Nothing (Just unauthRole)
|
||||
setupAuthMode' Nothing Nothing mempty (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing Nothing (Just fakeJWTConfig) (Just unauthRole)
|
||||
setupAuthMode' Nothing Nothing [fakeJWTConfig] (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) Nothing (Just unauthRole)
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) mempty (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) (Just fakeJWTConfig) (Just unauthRole)
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) [fakeJWTConfig] (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
|
||||
it "with admin secret provided" $ do
|
||||
setupAuthMode' (Just secret) Nothing Nothing Nothing
|
||||
setupAuthMode' (Just secret) Nothing mempty Nothing
|
||||
`shouldReturn` Right (AMAdminSecret secret Nothing)
|
||||
setupAuthMode' (Just secret) Nothing Nothing (Just unauthRole)
|
||||
setupAuthMode' (Just secret) Nothing mempty (Just unauthRole)
|
||||
`shouldReturn` Right (AMAdminSecret secret $ Just unauthRole)
|
||||
|
||||
setupAuthMode' (Just secret) Nothing (Just fakeJWTConfig) Nothing >>= \case
|
||||
setupAuthMode' (Just secret) Nothing [fakeJWTConfig] Nothing >>= \case
|
||||
Right (AMAdminSecretAndJWT s _ Nothing) -> do
|
||||
s `shouldBe` secret
|
||||
_ -> expectationFailure "AMAdminSecretAndJWT"
|
||||
setupAuthMode' (Just secret) Nothing (Just fakeJWTConfig) (Just unauthRole) >>= \case
|
||||
setupAuthMode' (Just secret) Nothing [fakeJWTConfig] (Just unauthRole) >>= \case
|
||||
Right (AMAdminSecretAndJWT s _ ur) -> do
|
||||
s `shouldBe` secret
|
||||
ur `shouldBe` Just unauthRole
|
||||
_ -> expectationFailure "AMAdminSecretAndJWT"
|
||||
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) Nothing Nothing
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) mempty Nothing
|
||||
`shouldReturn` Right (AMAdminSecretAndHook secret fakeAuthHook)
|
||||
-- auth hook can't make use of unauthenticated role for now (no good UX):
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) Nothing (Just unauthRole)
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) mempty (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
-- we can't have both:
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) (Just fakeJWTConfig) Nothing
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) [fakeJWTConfig] Nothing
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) (Just fakeJWTConfig) (Just unauthRole)
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) [fakeJWTConfig] (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
|
||||
parseClaimsMapTests :: Spec
|
||||
@ -628,10 +628,10 @@ instance Tracing.HasReporter NoReporter
|
||||
setupAuthMode' ::
|
||||
Maybe (HashSet AdminSecretHash) ->
|
||||
Maybe AuthHook ->
|
||||
Maybe JWTConfig ->
|
||||
[JWTConfig] ->
|
||||
Maybe RoleName ->
|
||||
IO (Either () AuthMode)
|
||||
setupAuthMode' mAdminSecretHash mWebHook mJwtSecret mUnAuthRole =
|
||||
setupAuthMode' mAdminSecretHash mWebHook jwtSecrets mUnAuthRole =
|
||||
-- just throw away the error message for ease of testing:
|
||||
fmap (either (const $ Left ()) Right) $
|
||||
runNoReporter $
|
||||
@ -640,7 +640,7 @@ setupAuthMode' mAdminSecretHash mWebHook mJwtSecret mUnAuthRole =
|
||||
setupAuthMode
|
||||
(fromMaybe Set.empty mAdminSecretHash)
|
||||
mWebHook
|
||||
mJwtSecret
|
||||
jwtSecrets
|
||||
mUnAuthRole
|
||||
-- NOTE: this won't do any http or launch threads if we don't specify JWT URL:
|
||||
(error "H.Manager")
|
||||
|
@ -199,7 +199,7 @@ serveOptions =
|
||||
soTxIso = Q.Serializable,
|
||||
soAdminSecret = mempty,
|
||||
soAuthHook = Nothing,
|
||||
soJwtSecret = Nothing,
|
||||
soJwtSecret = mempty,
|
||||
soUnAuthRole = Nothing,
|
||||
soCorsConfig = CCAllowAll,
|
||||
soEnableConsole = True,
|
||||
|
@ -9,7 +9,7 @@ response:
|
||||
- extensions:
|
||||
path: $
|
||||
code: invalid-headers
|
||||
message: Missing Cookie header in JWT authentication mode
|
||||
message: Missing 'Authorization' or 'Cookie' header in JWT authentication mode
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
|
@ -57,7 +57,7 @@ class TestConfigAPI():
|
||||
assert body['jwt']['claims_namespace'] == claims_namespace
|
||||
assert body['jwt']['claims_format'] == claims_format
|
||||
else:
|
||||
assert body['jwt'] == None
|
||||
assert body['jwt'] == []
|
||||
|
||||
# test if the request fails without auth headers if admin secret is set
|
||||
if admin_secret is not None:
|
||||
|
Loading…
Reference in New Issue
Block a user